package configkit import ( "errors" "math" "path/filepath" "reflect" "strings" "testing" "time" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) type overrideLifecycleSettings struct { Target string `yaml:"target"` Count int `yaml:"count"` Enabled bool `yaml:"enabled"` Text string `yaml:"text"` Items []string `yaml:"items"` Maybe *string `yaml:"maybe"` Labels map[string]string `yaml:"labels"` APIKey Secret `yaml:"api_key"` } type overrideLifecycleConfig struct { Version string `yaml:"version"` Settings overrideLifecycleSettings `yaml:"settings"` } func TestOverridesRunAtHighestInputPrecedenceBeforeResolutionAndValidation(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports: [{path: base.yml}]\nversion: v1\n") writeImportFile(t, directory, "base.yml", `settings: target: base count: 4 enabled: true text: base items: [base] maybe: base labels: {base: retained} api_key: BASE_SECRET `) writeImportFile(t, directory, filepath.Join("profiles", "testing.yml"), `settings: target: profile labels: {profile: retained} `) validated := false overrides := []Override{ {Path: Keys("settings", "target"), Value: " cli ", Flag: "--target"}, {Path: Keys("settings", "count"), Value: 0, Flag: "--count"}, {Path: Keys("settings", "enabled"), Value: false, Flag: "--enabled"}, {Path: Keys("settings", "text"), Value: "", Flag: "--text"}, {Path: Keys("settings", "items"), Value: []any{}, Flag: "--items"}, {Path: Keys("settings", "maybe"), Value: nil, Flag: "--maybe"}, {Path: Keys("settings", "labels"), Value: map[string]any{"cli": "selected"}, Flag: "--label"}, {Path: Keys("settings", "api_key"), Value: "CLI_SECRET", Flag: "--api-key-env"}, } result, err := loadWithSecretLookup(File(mainPath), func(name string) (string, bool) { if name != "CLI_SECRET" { t.Fatalf("looked up superseded secret name %q", name) } return "credential-value", true }, Options[overrideLifecycleConfig]{ Imports: true, Profile: "testing", Overrides: overrides, Definition: Definition[overrideLifecycleConfig]{ Defaults: func() overrideLifecycleConfig { return overrideLifecycleConfig{Settings: overrideLifecycleSettings{Target: "default", Count: 9, Text: "default", Items: []string{"default"}}} }, Version: &VersionRule{Path: Keys("version"), Check: func(version Version, validation *Validation) error { if version.Text != "v1" { validation.Error(Keys("version"), "unsupported version") } return nil }}, Resolve: func(configuration *overrideLifecycleConfig, resolution *Resolution) error { Normalize(resolution, Keys("settings", "target"), &configuration.Settings.Target, strings.TrimSpace(configuration.Settings.Target), "trimmed target") return nil }, Validate: func(configuration *overrideLifecycleConfig, validation *Validation) { validated = configuration.Settings.Target == "cli" && configuration.Settings.APIKey.Value() == "credential-value" }, }, }) if err != nil { t.Fatalf("loadWithSecretLookup() error = %v", err) } want := overrideLifecycleSettings{ Target: "cli", Labels: map[string]string{"base": "retained", "profile": "retained", "cli": "selected"}, APIKey: result.Config.Settings.APIKey, } if !validated || result.Config.Settings.Count != 0 || result.Config.Settings.Enabled || result.Config.Settings.Text != "" || len(result.Config.Settings.Items) != 0 || result.Config.Settings.Items == nil || result.Config.Settings.Maybe != nil || result.Config.Settings.Target != want.Target || !reflect.DeepEqual(result.Config.Settings.Labels, want.Labels) { t.Fatalf("effective settings = %#v", result.Config.Settings) } sources := result.Sources() if len(sources) != 3+len(overrides) || sources[0].Layer != LayerBase || sources[1].Layer != LayerBase || sources[2].Layer != LayerProfile { t.Fatalf("sources = %#v", sources) } for index, override := range overrides { if sources[index+3].Layer != LayerCLI || sources[index+3].Label != override.Flag || sources[index+3].Path != "" { t.Fatalf("CLI source %d = %#v", index, sources[index+3]) } } history := result.Metadata().History(Keys("settings", "target")) wantLayers := []Layer{LayerDefault, LayerBase, LayerProfile, LayerCLI, LayerResolution} if got := historyLayers(history); !reflect.DeepEqual(got, wantLayers) { t.Fatalf("target history layers = %#v, want %#v; history %#v", got, wantLayers, history) } } func TestOverridesSupportDefaultsOnlyMapMergeListReplacementAndNumericKeys(t *testing.T) { type config struct { Labels map[string]string `yaml:"labels"` Items []int `yaml:"items"` Numbers map[string]int `yaml:"numbers"` Dynamic any `yaml:"dynamic"` } labels := map[string]any{"0": "zero", "new": "value"} items := []any{0, 2} dynamic := map[string]any{ "int": int8(1), "uint": uint16(2), "float": float32(3.5), "bool": false, "null": nil, "list": []any{"value"}, "map": map[string]any{"key": "value"}, } result, err := Load(NoSource(), Options[config]{ Definition: Definition[config]{Defaults: func() config { return config{Labels: map[string]string{"kept": "default"}, Items: []int{9}} }}, Overrides: []Override{ {Path: parseOverrideTestPath(t, "/labels"), Value: labels, Flag: "--labels"}, {Path: Keys("items"), Value: items, Flag: "--items"}, {Path: parseOverrideTestPath(t, "/numbers/0"), Value: 7, Flag: "--number"}, {Path: Keys("dynamic"), Value: dynamic, Flag: "--dynamic"}, }, }) labels["new"] = "mutated" items[0] = 99 dynamic["int"] = 99 if err != nil { t.Fatal(err) } if !reflect.DeepEqual(result.Config.Labels, map[string]string{"kept": "default", "0": "zero", "new": "value"}) || !reflect.DeepEqual(result.Config.Items, []int{0, 2}) || result.Config.Numbers["0"] != 7 || result.Config.Dynamic.(map[string]any)["int"] != int64(1) { t.Fatalf("defaults-only overrides = %#v", result.Config) } var effective strings.Builder if err := result.WriteEffective(&effective); err != nil { t.Fatal(err) } if strings.Contains(effective.String(), "mutated") || !strings.Contains(effective.String(), "new: value") { t.Fatalf("detached effective output = %q", effective.String()) } if info, ok := result.Metadata().Lookup(Keys("labels", "0")); !ok || !info.Explicit || info.Origin.Layer != LayerCLI { t.Fatalf("numeric mapping key metadata = %#v, %v", info, ok) } } func TestOverrideValidationRejectsPathsOverlapValuesAndProtectedTargets(t *testing.T) { type config struct { Version string `yaml:"version"` Scalar string `yaml:"scalar"` Items []string `yaml:"items"` Nested map[string]any `yaml:"nested"` Unknown string `yaml:"unknown"` } base := Bytes("override-errors.yml", []byte("version: v1\nscalar: value\nitems: [one]\nnested: {}\n")) version := &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error { return nil }} cycle := map[string]any{} cycle["self"] = cycle deep := any("leaf") for range maxDocumentDepth + 1 { deep = []any{deep} } for _, test := range []struct { name string overrides []Override code Code }{ {name: "root", overrides: []Override{{Path: Keys(), Value: "x", Flag: "--root"}}, code: CodeOverrideInvalidPath}, {name: "index", overrides: []Override{{Path: Keys("items").Index(0), Value: "x", Flag: "--item"}}, code: CodeOverrideInvalidPath}, {name: "parsed sequence index", overrides: []Override{{Path: parseOverrideTestPath(t, "/items/0"), Value: "x", Flag: "--item"}}, code: CodeOverrideInvalidPath}, {name: "scalar ancestor", overrides: []Override{{Path: Keys("scalar", "child"), Value: "x", Flag: "--child"}}, code: CodeOverrideInvalidPath}, {name: "duplicate", overrides: []Override{{Path: Keys("scalar"), Value: "a", Flag: "--first"}, {Path: Keys("scalar"), Value: "b", Flag: "--second"}}, code: CodeOverrideDuplicate}, {name: "overlap", overrides: []Override{{Path: Keys("nested"), Value: map[string]any{}, Flag: "--nested"}, {Path: Keys("nested", "child"), Value: "x", Flag: "--child"}}, code: CodeOverrideOverlap}, {name: "unsupported", overrides: []Override{{Path: Keys("scalar"), Value: time.Second, Flag: "--scalar"}}, code: CodeOverrideInvalidValue}, {name: "cycle", overrides: []Override{{Path: Keys("nested"), Value: cycle, Flag: "--nested"}}, code: CodeOverrideInvalidValue}, {name: "nonfinite", overrides: []Override{{Path: Keys("scalar"), Value: math.Inf(1), Flag: "--scalar"}}, code: CodeOverrideInvalidValue}, {name: "depth", overrides: []Override{{Path: Keys("nested"), Value: deep, Flag: "--nested"}}, code: CodeOverrideLimitExceeded}, {name: "record bytes", overrides: []Override{{Path: Keys("scalar"), Value: strings.Repeat("x", maxSourceBytes+1), Flag: "--scalar"}}, code: CodeOverrideLimitExceeded}, {name: "directive path", overrides: []Override{{Path: Keys("configkit", "x"), Value: true, Flag: "--directive"}}, code: CodeOverrideDirective}, {name: "directive value", overrides: []Override{{Path: Keys("nested"), Value: map[string]any{"configkit": map[string]any{}}, Flag: "--nested"}}, code: CodeOverrideDirective}, {name: "version", overrides: []Override{{Path: Keys("version"), Value: "v2", Flag: "--version"}}, code: CodeOverrideVersion}, {name: "version ancestor", overrides: []Override{{Path: Keys("version"), Value: map[string]any{"child": "v2"}, Flag: "--version"}}, code: CodeOverrideVersion}, {name: "label", overrides: []Override{{Path: Keys("scalar"), Value: "x", Flag: "\nCANARY"}}, code: CodeOverrideInvalidLabel}, } { t.Run(test.name, func(t *testing.T) { result, err := Load(base, Options[config]{Definition: Definition[config]{Version: version}, Overrides: test.overrides}) if result != nil { t.Fatalf("result = %#v, want nil", result) } loadError := requireSecretLoadCode(t, err, test.code) if strings.Contains(fmtLoadError(loadError), strings.Repeat("x", 64)) { t.Fatal("override diagnostic leaked a raw value") } }) } } func TestOverrideCannotReplaceSchemaVersionAncestor(t *testing.T) { type schemaConfig struct { Version string `yaml:"version"` } type config struct { Schema schemaConfig `yaml:"schema"` } _, err := Load(Bytes("version.yml", []byte("schema: {version: v1}\n")), Options[config]{ Definition: Definition[config]{Version: &VersionRule{Path: Keys("schema", "version"), Check: func(Version, *Validation) error { return nil }}}, Overrides: []Override{{Path: Keys("schema"), Value: map[string]any{"other": "value"}, Flag: "--schema"}}, }) requireSecretLoadCode(t, err, CodeOverrideVersion) } func TestOverrideDecodeFailureUsesFlagSourceWithoutRawValue(t *testing.T) { type config struct { Count int `yaml:"count"` } result, err := Load(Bytes("base.yml", []byte("count: 1\n")), Options[config]{Overrides: []Override{{ Path: Keys("missing"), Value: "RAW-ARG-CANARY", Flag: "--missing", }}}) if result != nil { t.Fatalf("result = %#v", result) } var loadError *LoadError if !errors.As(err, &loadError) { t.Fatalf("error = %T", err) } diagnostics := loadError.Diagnostics() sources := loadError.Sources() if len(diagnostics) != 1 || diagnostics[0].Code != CodeDecodeUnknownField || diagnostics[0].Primary == nil || len(sources) != 2 || sources[1].Label != "--missing" || diagnostics[0].Primary.SourceID != sources[1].ID { t.Fatalf("diagnostics/sources = %#v / %#v", diagnostics, sources) } if strings.Contains(fmtLoadError(loadError), "RAW-ARG-CANARY") { t.Fatal("decode failure leaked override value") } } func TestOverrideTypeMismatchUsesNormalStrictDecode(t *testing.T) { type config struct { Count int `yaml:"count"` } _, err := Load(Bytes("base.yml", []byte("count: 1\n")), Options[config]{Overrides: []Override{{ Path: Keys("count"), Value: "not-an-integer", Flag: "--count", }}}) loadError := requireSecretLoadCode(t, err, CodeDecodeType) if loadError.Diagnostics()[0].Primary == nil || loadError.Sources()[1].Label != "--count" || strings.Contains(fmtLoadError(loadError), "not-an-integer") { t.Fatalf("type mismatch diagnostics/sources = %#v / %#v", loadError.Diagnostics(), loadError.Sources()) } } func TestOverrideEqualReplacementStillAppearsInHistory(t *testing.T) { type config struct { Value string `yaml:"value"` } result, err := Load(Bytes("equal.yml", []byte("value: same\n")), Options[config]{Overrides: []Override{{Path: Keys("value"), Value: "same", Flag: "--value"}}}) if err != nil { t.Fatal(err) } if got := historyLayers(result.Metadata().History(Keys("value"))); !reflect.DeepEqual(got, []Layer{LayerDefault, LayerBase, LayerCLI}) { t.Fatalf("equal override history = %#v", result.Metadata().History(Keys("value"))) } } func TestOverrideAggregateLimitsFailBeforeApplication(t *testing.T) { base, diagnostics := composeDocuments(&document.Node{Kind: document.Mapping}) if len(diagnostics) != 0 { t.Fatal(diagnostics) } base.inputNodes = 1 result, sources, diagnostics := applyOverrides(base, nil, []Override{{Path: Keys("value"), Value: "x", Flag: "--value"}}, nil, importLimits{sources: 1, bytes: 1, nodes: 2}) if result != nil || len(sources) != 1 || len(diagnostics) != 1 || diagnostics[0].Code != CodeOverrideLimitExceeded || len(base.root.Mapping) != 0 { t.Fatalf("bounded apply = %#v, %#v, %#v; base %#v", result, sources, diagnostics, base.root) } } func historyLayers(history []Change) []Layer { layers := make([]Layer, len(history)) for index, change := range history { layers[index] = change.Origin.Layer } return layers } func parseOverrideTestPath(t *testing.T, value string) Path { t.Helper() path, err := ParsePath(value) if err != nil { t.Fatal(err) } return path } func fmtLoadError(loadError *LoadError) string { var output strings.Builder _ = WriteDiagnostics(&output, loadError.Diagnostics()) return loadError.Error() + output.String() }