diff --git a/internal/api/system_settings_utils_test.go b/internal/api/system_settings_utils_test.go new file mode 100644 index 0000000..f804743 --- /dev/null +++ b/internal/api/system_settings_utils_test.go @@ -0,0 +1,516 @@ +package api + +import ( + "testing" +) + +func TestFirstValueForKeys(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + m map[string]interface{} + keys []string + wantVal interface{} + wantBool bool + }{ + // Basic functionality + { + name: "single key found", + m: map[string]interface{}{"foo": "bar"}, + keys: []string{"foo"}, + wantVal: "bar", + wantBool: true, + }, + { + name: "single key not found", + m: map[string]interface{}{"foo": "bar"}, + keys: []string{"baz"}, + wantVal: nil, + wantBool: false, + }, + { + name: "first of multiple keys found", + m: map[string]interface{}{"camelCase": "value1", "snake_case": "value2"}, + keys: []string{"camelCase", "snake_case"}, + wantVal: "value1", + wantBool: true, + }, + { + name: "second key found when first missing", + m: map[string]interface{}{"snake_case": "value2"}, + keys: []string{"camelCase", "snake_case"}, + wantVal: "value2", + wantBool: true, + }, + { + name: "third key found when first two missing", + m: map[string]interface{}{"third": "value3"}, + keys: []string{"first", "second", "third"}, + wantVal: "value3", + wantBool: true, + }, + // Edge cases + { + name: "nil map", + m: nil, + keys: []string{"foo"}, + wantVal: nil, + wantBool: false, + }, + { + name: "empty map", + m: map[string]interface{}{}, + keys: []string{"foo"}, + wantVal: nil, + wantBool: false, + }, + { + name: "empty keys slice", + m: map[string]interface{}{"foo": "bar"}, + keys: []string{}, + wantVal: nil, + wantBool: false, + }, + { + name: "nil keys slice", + m: map[string]interface{}{"foo": "bar"}, + keys: nil, + wantVal: nil, + wantBool: false, + }, + { + name: "empty string key", + m: map[string]interface{}{"": "empty-key-value"}, + keys: []string{""}, + wantVal: "empty-key-value", + wantBool: true, + }, + // Value types + { + name: "nil value is found", + m: map[string]interface{}{"foo": nil}, + keys: []string{"foo"}, + wantVal: nil, + wantBool: true, + }, + { + name: "integer value", + m: map[string]interface{}{"count": 42}, + keys: []string{"count"}, + wantVal: 42, + wantBool: true, + }, + { + name: "float value", + m: map[string]interface{}{"ratio": 3.14}, + keys: []string{"ratio"}, + wantVal: 3.14, + wantBool: true, + }, + { + name: "bool value", + m: map[string]interface{}{"enabled": true}, + keys: []string{"enabled"}, + wantVal: true, + wantBool: true, + }, + { + name: "nested map value", + m: map[string]interface{}{"nested": map[string]interface{}{"inner": "data"}}, + keys: []string{"nested"}, + wantVal: map[string]interface{}{"inner": "data"}, + wantBool: true, + }, + { + name: "slice value", + m: map[string]interface{}{"items": []string{"a", "b", "c"}}, + keys: []string{"items"}, + wantVal: []string{"a", "b", "c"}, + wantBool: true, + }, + // Priority order matters + { + name: "priority prefers first key even if second also exists", + m: map[string]interface{}{"a": "first", "b": "second"}, + keys: []string{"a", "b"}, + wantVal: "first", + wantBool: true, + }, + { + name: "priority skips nil value keys - nil value is still returned", + m: map[string]interface{}{"a": nil, "b": "second"}, + keys: []string{"a", "b"}, + wantVal: nil, + wantBool: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotVal, gotBool := firstValueForKeys(tt.m, tt.keys...) + if gotBool != tt.wantBool { + t.Errorf("firstValueForKeys() bool = %v, want %v", gotBool, tt.wantBool) + } + // For comparing interface{} values + if !equalInterface(gotVal, tt.wantVal) { + t.Errorf("firstValueForKeys() val = %v (%T), want %v (%T)", gotVal, gotVal, tt.wantVal, tt.wantVal) + } + }) + } +} + +func TestHasAnyKey(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + m map[string]interface{} + keys []string + want bool + }{ + // Basic functionality + { + name: "single key exists", + m: map[string]interface{}{"foo": "bar"}, + keys: []string{"foo"}, + want: true, + }, + { + name: "single key does not exist", + m: map[string]interface{}{"foo": "bar"}, + keys: []string{"baz"}, + want: false, + }, + { + name: "first of multiple keys exists", + m: map[string]interface{}{"first": "value"}, + keys: []string{"first", "second"}, + want: true, + }, + { + name: "second of multiple keys exists", + m: map[string]interface{}{"second": "value"}, + keys: []string{"first", "second"}, + want: true, + }, + { + name: "last of multiple keys exists", + m: map[string]interface{}{"third": "value"}, + keys: []string{"first", "second", "third"}, + want: true, + }, + { + name: "none of multiple keys exist", + m: map[string]interface{}{"other": "value"}, + keys: []string{"first", "second", "third"}, + want: false, + }, + // Edge cases + { + name: "nil map", + m: nil, + keys: []string{"foo"}, + want: false, + }, + { + name: "empty map", + m: map[string]interface{}{}, + keys: []string{"foo"}, + want: false, + }, + { + name: "empty keys slice", + m: map[string]interface{}{"foo": "bar"}, + keys: []string{}, + want: false, + }, + { + name: "nil keys slice", + m: map[string]interface{}{"foo": "bar"}, + keys: nil, + want: false, + }, + { + name: "empty string key exists", + m: map[string]interface{}{"": "empty"}, + keys: []string{""}, + want: true, + }, + // Key exists with nil value + { + name: "key with nil value counts as existing", + m: map[string]interface{}{"foo": nil}, + keys: []string{"foo"}, + want: true, + }, + // Real-world usage: camelCase vs snake_case + { + name: "discoveryConfig camelCase exists", + m: map[string]interface{}{"discoveryConfig": map[string]interface{}{}}, + keys: []string{"discoveryConfig", "discovery_config"}, + want: true, + }, + { + name: "discovery_config snake_case exists", + m: map[string]interface{}{"discovery_config": map[string]interface{}{}}, + keys: []string{"discoveryConfig", "discovery_config"}, + want: true, + }, + { + name: "neither naming convention exists", + m: map[string]interface{}{"otherConfig": map[string]interface{}{}}, + keys: []string{"discoveryConfig", "discovery_config"}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := hasAnyKey(tt.m, tt.keys...); got != tt.want { + t.Errorf("hasAnyKey() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDiscoveryConfigMap(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + raw map[string]interface{} + wantMap map[string]interface{} + wantBool bool + }{ + // CamelCase variant + { + name: "discoveryConfig with valid map", + raw: map[string]interface{}{ + "discoveryConfig": map[string]interface{}{ + "enabled": true, + "subnet": "192.168.1.0/24", + }, + }, + wantMap: map[string]interface{}{"enabled": true, "subnet": "192.168.1.0/24"}, + wantBool: true, + }, + { + name: "discoveryConfig with empty map", + raw: map[string]interface{}{ + "discoveryConfig": map[string]interface{}{}, + }, + wantMap: map[string]interface{}{}, + wantBool: true, + }, + // Snake_case variant + { + name: "discovery_config with valid map", + raw: map[string]interface{}{ + "discovery_config": map[string]interface{}{ + "enabled": false, + "timeout": 30, + }, + }, + wantMap: map[string]interface{}{"enabled": false, "timeout": 30}, + wantBool: true, + }, + { + name: "discovery_config with empty map", + raw: map[string]interface{}{ + "discovery_config": map[string]interface{}{}, + }, + wantMap: map[string]interface{}{}, + wantBool: true, + }, + // Priority: camelCase over snake_case + { + name: "camelCase takes priority over snake_case", + raw: map[string]interface{}{ + "discoveryConfig": map[string]interface{}{"from": "camel"}, + "discovery_config": map[string]interface{}{"from": "snake"}, + }, + wantMap: map[string]interface{}{"from": "camel"}, + wantBool: true, + }, + // Not found + { + name: "neither key exists", + raw: map[string]interface{}{"otherKey": "value"}, + wantMap: nil, + wantBool: false, + }, + { + name: "empty raw map", + raw: map[string]interface{}{}, + wantMap: nil, + wantBool: false, + }, + { + name: "nil raw map", + raw: nil, + wantMap: nil, + wantBool: false, + }, + // Type assertion failures - key exists but not a map + { + name: "discoveryConfig is string not map", + raw: map[string]interface{}{ + "discoveryConfig": "not-a-map", + }, + wantMap: nil, + wantBool: true, // key found, but type assertion failed + }, + { + name: "discoveryConfig is int not map", + raw: map[string]interface{}{ + "discoveryConfig": 42, + }, + wantMap: nil, + wantBool: true, + }, + { + name: "discoveryConfig is slice not map", + raw: map[string]interface{}{ + "discoveryConfig": []string{"a", "b"}, + }, + wantMap: nil, + wantBool: true, + }, + { + name: "discoveryConfig is nil", + raw: map[string]interface{}{ + "discoveryConfig": nil, + }, + wantMap: nil, + wantBool: true, + }, + { + name: "discovery_config is string not map", + raw: map[string]interface{}{ + "discovery_config": "also-not-a-map", + }, + wantMap: nil, + wantBool: true, + }, + { + name: "discovery_config is bool not map", + raw: map[string]interface{}{ + "discovery_config": true, + }, + wantMap: nil, + wantBool: true, + }, + // Nested map structure + { + name: "deeply nested config values", + raw: map[string]interface{}{ + "discoveryConfig": map[string]interface{}{ + "nested": map[string]interface{}{ + "deep": "value", + }, + "array": []int{1, 2, 3}, + }, + }, + wantMap: map[string]interface{}{ + "nested": map[string]interface{}{"deep": "value"}, + "array": []int{1, 2, 3}, + }, + wantBool: true, + }, + // Edge case: wrong type map (not map[string]interface{}) + { + name: "discoveryConfig is map[string]string not map[string]interface{}", + raw: map[string]interface{}{ + "discoveryConfig": map[string]string{"key": "value"}, + }, + wantMap: nil, + wantBool: true, // key found, type assertion to map[string]interface{} fails + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotMap, gotBool := discoveryConfigMap(tt.raw) + if gotBool != tt.wantBool { + t.Errorf("discoveryConfigMap() bool = %v, want %v", gotBool, tt.wantBool) + } + if !equalMapInterface(gotMap, tt.wantMap) { + t.Errorf("discoveryConfigMap() map = %v, want %v", gotMap, tt.wantMap) + } + }) + } +} + +// equalInterface compares two interface{} values for equality +func equalInterface(a, b interface{}) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + + // Handle map[string]interface{} specially + aMap, aIsMap := a.(map[string]interface{}) + bMap, bIsMap := b.(map[string]interface{}) + if aIsMap && bIsMap { + return equalMapInterface(aMap, bMap) + } + + // Handle []string specially + aSlice, aIsSlice := a.([]string) + bSlice, bIsSlice := b.([]string) + if aIsSlice && bIsSlice { + if len(aSlice) != len(bSlice) { + return false + } + for i := range aSlice { + if aSlice[i] != bSlice[i] { + return false + } + } + return true + } + + // Handle []int specially + aIntSlice, aIsIntSlice := a.([]int) + bIntSlice, bIsIntSlice := b.([]int) + if aIsIntSlice && bIsIntSlice { + if len(aIntSlice) != len(bIntSlice) { + return false + } + for i := range aIntSlice { + if aIntSlice[i] != bIntSlice[i] { + return false + } + } + return true + } + + // Default comparison (may panic for uncomparable types, but those should be handled above) + return a == b +} + +// equalMapInterface compares two map[string]interface{} values +func equalMapInterface(a, b map[string]interface{}) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + if len(a) != len(b) { + return false + } + for k, v := range a { + bv, ok := b[k] + if !ok { + return false + } + if !equalInterface(v, bv) { + return false + } + } + return true +}