package engine import ( "errors" "math" "reflect" "strings" "testing" "time" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) type namedSmall int8 type validatedText string func (value validatedText) MarshalText() ([]byte, error) { return []byte(value), nil } func (value *validatedText) UnmarshalText(text []byte) error { if string(text) == "bad-secret-canary" { return errors.New("CANARY_TEXT_ERROR: bad-secret-canary") } *value = validatedText(text) return nil } func TestDecodeAppliesInputOverDetachedDefaults(t *testing.T) { t.Parallel() type nested struct { A string `yaml:"a"` B string `yaml:"b"` } type config struct { Enabled bool `yaml:"enabled"` Workers int `yaml:"workers"` Label string `yaml:"label"` Nested nested `yaml:"nested"` EmptyMapping nested `yaml:"empty_mapping"` Objects map[string]nested `yaml:"objects"` Sequence []string `yaml:"sequence"` Replacement []string `yaml:"replacement"` Pointer *nested `yaml:"pointer"` NilPointer *nested `yaml:"nil_pointer"` Nullable map[string]string `yaml:"nullable"` Dynamic any `yaml:"dynamic"` DynamicShape any `yaml:"dynamic_shape"` DynamicNull any `yaml:"dynamic_null"` Fixed [2]int `yaml:"fixed"` Small namedSmall `yaml:"small"` Delay time.Duration `yaml:"delay"` Custom validatedText `yaml:"custom"` } defaults := config{ Enabled: true, Workers: 4, Label: "default", Nested: nested{A: "keep", B: "before"}, EmptyMapping: nested{A: "keep-empty", B: "keep-empty"}, Objects: map[string]nested{ "existing": {A: "keep-map", B: "before"}, }, Sequence: []string{"default"}, Replacement: []string{"default"}, Pointer: &nested{A: "keep-pointer", B: "before"}, Nullable: map[string]string{"default": "value"}, Dynamic: map[string]any{"keep": "default", "count": int64(4)}, DynamicShape: nested{A: "keep-dynamic", B: "before"}, DynamicNull: "default", Fixed: [2]int{1, 2}, Small: 7, Delay: time.Second, Custom: "default", } input := ` enabled: false workers: 0 label: "" nested: {b: changed} empty_mapping: {} objects: existing: {b: changed} new: {b: added} sequence: [] replacement: [one, two] pointer: {b: changed} nil_pointer: {b: added} nullable: null dynamic: {count: 0, fraction: 1.5} dynamic_shape: {b: changed} dynamic_null: null fixed: [3, 4] small: -8 delay: 1.5s custom: accepted ` schema := mustSchemaFor[config](t) documentRoot := parseDocumentForDecode(t, input) decoded, issues := decodeConfiguration(schema, reflect.ValueOf(defaults), documentRoot) if len(issues) != 0 { t.Fatalf("decode issues = %#v", issues) } got := decoded.value.Interface().(config) if got.Enabled || got.Workers != 0 || got.Label != "" { t.Fatalf("explicit zero values lost: %#v", got) } if got.Nested != (nested{A: "keep", B: "changed"}) || got.EmptyMapping != defaults.EmptyMapping { t.Fatalf("mapping defaults = %#v / %#v", got.Nested, got.EmptyMapping) } if got.Objects["existing"] != (nested{A: "keep-map", B: "changed"}) || got.Objects["new"] != (nested{B: "added"}) { t.Fatalf("map defaults = %#v", got.Objects) } if got.Sequence == nil || len(got.Sequence) != 0 || !reflect.DeepEqual(got.Replacement, []string{"one", "two"}) { t.Fatalf("sequence replacement = %#v / %#v", got.Sequence, got.Replacement) } if got.Pointer == defaults.Pointer || *got.Pointer != (nested{A: "keep-pointer", B: "changed"}) || got.NilPointer == nil || *got.NilPointer != (nested{B: "added"}) { t.Fatalf("pointer defaults = %#v / %#v", got.Pointer, got.NilPointer) } if got.Nullable != nil || got.DynamicNull != nil { t.Fatalf("null values = %#v / %#v", got.Nullable, got.DynamicNull) } dynamic := got.Dynamic.(map[string]any) if dynamic["keep"] != "default" || dynamic["count"] != int64(0) || dynamic["fraction"] != float64(1.5) { t.Fatalf("dynamic mapping = %#v", dynamic) } if got.DynamicShape != (nested{A: "keep-dynamic", B: "changed"}) { t.Fatalf("dynamic concrete mapping = %#v", got.DynamicShape) } if got.Fixed != [2]int{3, 4} || got.Small != -8 || got.Delay != 1500*time.Millisecond || got.Custom != "accepted" { t.Fatalf("scalar decoding = %#v", got) } for _, path := range []Path{Keys(), Keys("enabled"), Keys("nested"), Keys("nested", "b"), Keys("empty_mapping")} { if _, ok := decoded.presence[path.String()]; !ok { t.Errorf("explicit path %q missing", path.String()) } } if _, ok := decoded.presence[Keys("nested", "a").String()]; ok { t.Fatal("parent mapping made omitted child explicit") } if decoded.origins[Keys("nested", "a").String()].Layer != LayerDefault || decoded.origins[Keys("nested", "b").String()].Layer != LayerBase { t.Fatalf("origins = %#v", decoded.origins) } if decoded.origins[Keys("nested", "b").String()].Locations[0].Line == 0 { t.Fatal("input origin lost source coordinates") } defaults.Objects["existing"] = nested{A: "mutated"} defaults.Pointer.B = "mutated" defaults.Sequence[0] = "mutated" if got.Objects["existing"].A != "keep-map" || got.Pointer.B != "changed" || len(got.Sequence) != 0 { t.Fatal("decoded value aliases defaults") } got.Pointer.B = "caller mutation" if mappingValue(t, mappingValue(t, decoded.final, "pointer"), "b").Text != "changed" { t.Fatal("final snapshot aliases returned typed value") } if mappingValue(t, mappingValue(t, decoded.baseline, "nested"), "b").Text != "before" { t.Fatal("baseline snapshot was mutated by decode") } } func TestDecodeDefaultsOnlyAndRepeatedResultsAreIndependent(t *testing.T) { t.Parallel() type config struct { Values map[string][]string `yaml:"values"` } defaults := config{Values: map[string][]string{"one": {"a"}}} schema := mustSchemaFor[config](t) first, issues := decodeConfiguration(schema, reflect.ValueOf(defaults), nil) if len(issues) != 0 { t.Fatal(issues) } second, issues := decodeConfiguration(schema, reflect.ValueOf(defaults), nil) if len(issues) != 0 { t.Fatal(issues) } firstValue := first.value.Interface().(config) secondValue := second.value.Interface().(config) firstValue.Values["one"][0] = "changed" if secondValue.Values["one"][0] != "a" || defaults.Values["one"][0] != "a" { t.Fatal("repeated defaults-only results share aliases") } if len(first.presence) != 0 || first.origins[Keys("values", "one").String()].Layer != LayerDefault { t.Fatalf("defaults-only metadata = %#v / %#v", first.presence, first.origins) } } func TestDecodeCollectsIndependentTypeAndShapeFailures(t *testing.T) { t.Parallel() type nested struct { Known int `yaml:"known"` } type config struct { Small int8 `yaml:"small"` Unsigned uint8 `yaml:"unsigned"` Number float32 `yaml:"number"` Text string `yaml:"text"` Flag bool `yaml:"flag"` Delay time.Duration `yaml:"delay"` Fixed [2]int `yaml:"fixed"` Nested nested `yaml:"nested"` Custom validatedText `yaml:"custom"` Dynamic any `yaml:"dynamic"` } input := ` small: 128 unsigned: -1 number: 1e999 text: 3 flag: "true" delay: 10 fixed: [1] nested: {first_unknown: 1, second_unknown: 2} custom: bad-secret-canary dynamic: 9223372036854775808 ` schema := mustSchemaFor[config](t) _, issues := decodeConfiguration(schema, reflect.ValueOf(config{Custom: "ok"}), parseDocumentForDecode(t, input)) if len(issues) != 11 { t.Fatalf("issues = %d: %#v", len(issues), issues) } want := map[string]Code{ "/small": CodeDecodeRange, "/unsigned": CodeDecodeType, "/number": CodeDecodeRange, "/text": CodeDecodeType, "/flag": CodeDecodeType, "/delay": CodeDecodeType, "/fixed": CodeDecodeArrayLength, "/nested/first_unknown": CodeDecodeUnknownField, "/nested/second_unknown": CodeDecodeUnknownField, "/custom": CodeDecodeText, "/dynamic": CodeDecodeRange, } for _, issue := range issues { if wantCode, ok := want[issue.path.String()]; !ok || wantCode != issue.code { t.Errorf("unexpected issue %#v", issue) } if issue.location.Line == 0 { t.Errorf("issue lost input location: %#v", issue) } if strings.Contains(issue.message, "CANARY") || strings.Contains(issue.message, "bad-secret") { t.Errorf("issue exposed value: %#v", issue) } } } func TestDecodeNullabilityAndNestedMapStrictness(t *testing.T) { t.Parallel() type nested struct { Value int `yaml:"value"` } type config struct { Scalar int `yaml:"scalar"` Object nested `yaml:"object"` Array [1]int `yaml:"array"` Map map[string]nested `yaml:"map"` } input := "scalar: null\nobject: null\narray: null\nmap:\n item: {unknown: 1}\n" _, issues := decodeConfiguration(mustSchemaFor[config](t), reflect.ValueOf(config{}), parseDocumentForDecode(t, input)) want := map[string]Code{ "/scalar": CodeDecodeNull, "/object": CodeDecodeNull, "/array": CodeDecodeNull, "/map/item/unknown": CodeDecodeUnknownField, } if len(issues) != len(want) { t.Fatalf("issues = %#v", issues) } for _, issue := range issues { if want[issue.path.String()] != issue.code { t.Errorf("issue = %#v", issue) } } } func TestDecodeRejectsInvalidDefaultsBeforeApplyingInput(t *testing.T) { t.Parallel() type config struct { Name string `yaml:"name"` Extras map[string]any `yaml:",inline"` Float float64 `yaml:"float"` } schema := mustSchemaFor[config](t) for name, defaults := range map[string]config{ "inline collision": {Name: "declared", Extras: map[string]any{"name": "duplicate"}}, "non-finite": {Float: math.NaN()}, } { t.Run(name, func(t *testing.T) { if _, issues := decodeConfiguration(schema, reflect.ValueOf(defaults), parseDocumentForDecode(t, "{}\n")); len(issues) != 1 || !strings.HasPrefix(string(issues[0].code), "schema.") { t.Fatalf("issues = %#v", issues) } }) } } func parseDocumentForDecode(t *testing.T, input string) *document.Node { t.Helper() parsed, diagnostics := parseSource(Bytes("decode.yml", []byte(input)), "source-1", defaultParseLimits()) if len(diagnostics) != 0 { t.Fatalf("parse diagnostics = %#v", diagnostics) } return parsed.document }