@@ -6,8 +6,10 @@ package mmodel
66
77import (
88 "encoding/json"
9+ "errors"
910 "testing"
1011
12+ pkg "github.com/LerianStudio/midaz/v3/pkg"
1113 "github.com/stretchr/testify/assert"
1214 "github.com/stretchr/testify/require"
1315)
@@ -299,6 +301,7 @@ func TestValidateSettings(t *testing.T) {
299301 input map [string ]any
300302 wantErr bool
301303 errContains string
304+ wantErrCode string // structured error code to assert, if non-empty
302305 }{
303306 {
304307 name : "nil settings returns no error" ,
@@ -391,6 +394,46 @@ func TestValidateSettings(t *testing.T) {
391394 wantErr : true ,
392395 errContains : "validateAccountType" ,
393396 },
397+ {
398+ name : "root-level validateAccountType returns error with parent key in message" ,
399+ input : map [string ]any {
400+ "validateAccountType" : true ,
401+ },
402+ wantErr : true ,
403+ errContains : "accounting" ,
404+ wantErrCode : "0149" ,
405+ },
406+ {
407+ name : "root-level validateRoutes returns error with field name in message" ,
408+ input : map [string ]any {
409+ "validateRoutes" : false ,
410+ },
411+ wantErr : true ,
412+ errContains : "validateRoutes" ,
413+ wantErrCode : "0149" ,
414+ },
415+ {
416+ name : "mixed root-level and nested returns error for root-level" ,
417+ input : map [string ]any {
418+ "validateAccountType" : true ,
419+ "accounting" : map [string ]any {
420+ "validateRoutes" : true ,
421+ },
422+ },
423+ wantErr : true ,
424+ errContains : "validateAccountType" ,
425+ wantErrCode : "0149" ,
426+ },
427+ {
428+ name : "multiple root-level fields returns error for first alphabetically" ,
429+ input : map [string ]any {
430+ "validateAccountType" : false ,
431+ "validateRoutes" : true ,
432+ },
433+ wantErr : true ,
434+ errContains : "validateAccountType" , // Deterministic: alphabetically first field is reported
435+ wantErrCode : "0149" ,
436+ },
394437 }
395438
396439 for _ , tt := range tests {
@@ -399,6 +442,13 @@ func TestValidateSettings(t *testing.T) {
399442
400443 if tt .wantErr {
401444 require .Error (t , err )
445+
446+ if tt .wantErrCode != "" {
447+ var vErr pkg.ValidationError
448+ require .True (t , errors .As (err , & vErr ), "expected ValidationError type, got %T" , err )
449+ assert .Equal (t , tt .wantErrCode , vErr .Code , "expected error code %q, got %q" , tt .wantErrCode , vErr .Code )
450+ }
451+
402452 assert .Contains (t , err .Error (), tt .errContains )
403453 } else {
404454 require .NoError (t , err )
@@ -591,6 +641,57 @@ func TestDeepMergeSettings(t *testing.T) {
591641 }
592642}
593643
644+ // TestSettingsSchema_NoDuplicateNestedFieldNames validates that settingsSchema has no duplicate
645+ // nested field names across different parent keys. If two parent keys define the same nested
646+ // field name, knownNestedFieldNames would have nondeterministic behavior due to map iteration order.
647+ // This test catches such issues at CI/CD time before deployment.
648+ func TestSettingsSchema_NoDuplicateNestedFieldNames (t * testing.T ) {
649+ // Track which parent key owns each field name
650+ fieldToParent := make (map [string ]string )
651+
652+ for parentKey , nestedFields := range settingsSchema {
653+ for fieldName := range nestedFields {
654+ if existingParent , exists := fieldToParent [fieldName ]; exists {
655+ // Build suggestion safely, handling empty fieldName
656+ suggestion := parentKey
657+ if fieldName != "" {
658+ suggestion = parentKey + "." + fieldName
659+ }
660+
661+ t .Fatalf (
662+ "settingsSchema has duplicate nested field name %q: defined in both %q and %q. " +
663+ "This causes nondeterministic behavior in knownNestedFieldNames. " +
664+ "Use unique field names or qualified names (e.g., %q instead of %q)." ,
665+ fieldName ,
666+ existingParent ,
667+ parentKey ,
668+ suggestion ,
669+ fieldName ,
670+ )
671+ }
672+
673+ fieldToParent [fieldName ] = parentKey
674+ }
675+ }
676+
677+ // Also verify knownNestedFieldNames was built correctly
678+ for fieldName , expectedParent := range fieldToParent {
679+ actualParent , exists := knownNestedFieldNames [fieldName ]
680+ if ! exists {
681+ t .Errorf ("knownNestedFieldNames missing field %q (expected parent: %q)" , fieldName , expectedParent )
682+ } else if actualParent != expectedParent {
683+ t .Errorf ("knownNestedFieldNames[%q] = %q, expected %q" , fieldName , actualParent , expectedParent )
684+ }
685+ }
686+
687+ // Verify no extra fields in knownNestedFieldNames
688+ for fieldName := range knownNestedFieldNames {
689+ if _ , exists := fieldToParent [fieldName ]; ! exists {
690+ t .Errorf ("knownNestedFieldNames contains unexpected field %q" , fieldName )
691+ }
692+ }
693+ }
694+
594695// FuzzValidateSettings verifies ValidateSettings never panics on arbitrary JSON input.
595696// It uses JSON strings as fuzz input since Go's fuzzing only supports primitive types.
596697// The function must either return nil (valid) or an error (invalid) - never panic.
@@ -620,6 +721,11 @@ func FuzzValidateSettings(f *testing.F) {
620721 `123` ,
621722 `true` ,
622723 `false` ,
724+ // Root-level field cases (should be nested under parent key)
725+ `{"validateAccountType": true}` ,
726+ `{"validateRoutes": false}` ,
727+ `{"validateAccountType": true, "validateRoutes": false}` ,
728+ `{"validateAccountType": true, "accounting": {"validateRoutes": true}}` ,
623729 }
624730
625731 for _ , seed := range seeds {
0 commit comments