Files
ConfigKit/internal/engine/override_test.go

689 lines
30 KiB
Go

package engine
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("overlays", "testing.yml"), `settings:
target: overlay
labels: {overlay: 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 := testLoadWithSecretLookup(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, Overlay: "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("testLoadWithSecretLookup() error = %v", err)
}
want := overrideLifecycleSettings{
Target: "cli", Labels: map[string]string{"base": "retained", "overlay": "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 != LayerOverlay {
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, LayerOverlay, 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 := testLoad(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: 4\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 := testLoad(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 TestOverrideOverlapIndexPreservesFirstAcceptedSource(t *testing.T) {
type config struct {
Nested map[string]any `yaml:"nested"`
}
base := Bytes("override-overlap.yml", []byte("nested: {}\n"))
_, err := testLoad(base, Options[config]{Overrides: []Override{
{Path: Keys("nested"), Value: map[string]any{}, Flag: "--nested"},
{Path: Keys("nested", "first"), Value: "one", Flag: "--first"},
{Path: Keys("nested", "second"), Value: "two", Flag: "--second"},
}})
if err == nil {
t.Fatal("load succeeded with overlapping overrides")
}
var loadErr *LoadError
if !errors.As(err, &loadErr) {
t.Fatalf("error = %T %v, want *LoadError", err, err)
}
diagnostics := loadErr.Diagnostics()
var conflicts []Diagnostic
for _, diagnostic := range diagnostics {
if diagnostic.Code == CodeOverrideOverlap {
conflicts = append(conflicts, diagnostic)
}
}
if len(conflicts) != 2 {
t.Fatalf("overlap diagnostics = %#v, want two conflicts", diagnostics)
}
for index, path := range []string{"/nested/first", "/nested/second"} {
if conflicts[index].Path.String() != path || len(conflicts[index].Related) != 1 || conflicts[index].Related[0].SourceID != "source-2" {
t.Fatalf("conflict[%d] = %#v, want path %q related to first CLI source", index, conflicts[index], path)
}
}
}
func TestOverrideOverlapIndexIncludesAcceptedPathWithInvalidValue(t *testing.T) {
type config struct {
Nested map[string]any `yaml:"nested"`
}
base := Bytes("override-value-overlap.yml", []byte("nested: {}\n"))
_, err := testLoad(base, Options[config]{Overrides: []Override{
{Path: Keys("nested"), Value: time.Second, Flag: "--nested"},
{Path: Keys("nested", "child"), Value: "value", Flag: "--child"},
}})
if err == nil {
t.Fatal("load succeeded with an invalid value and overlapping path")
}
var loadErr *LoadError
if !errors.As(err, &loadErr) {
t.Fatalf("error = %T %v, want *LoadError", err, err)
}
var invalidValue, overlap bool
for _, diagnostic := range loadErr.Diagnostics() {
switch diagnostic.Code {
case CodeOverrideInvalidValue:
invalidValue = diagnostic.Path.String() == "/nested"
case CodeOverrideOverlap:
overlap = diagnostic.Path.String() == "/nested/child" && len(diagnostic.Related) == 1 && diagnostic.Related[0].SourceID == "source-2"
}
}
if !invalidValue || !overlap {
t.Fatalf("diagnostics = %#v, want invalid parent value and child overlap related to its source", loadErr.Diagnostics())
}
}
func TestRejectedOverridePathDoesNotCauseOverlap(t *testing.T) {
type config struct {
Scalar string `yaml:"scalar"`
}
base := Bytes("override-rejected-overlap.yml", []byte("scalar: value\n"))
_, err := testLoad(base, Options[config]{Overrides: []Override{
{Path: Keys("scalar", "child"), Value: "invalid", Flag: "--child"},
{Path: Keys("scalar"), Value: "valid", Flag: "--scalar"},
}})
if err == nil {
t.Fatal("load succeeded with a path traversing a scalar")
}
var loadErr *LoadError
if !errors.As(err, &loadErr) {
t.Fatalf("error = %T %v, want *LoadError", err, err)
}
diagnostics := loadErr.Diagnostics()
if len(diagnostics) != 1 || diagnostics[0].Code != CodeOverrideInvalidPath || diagnostics[0].Path.String() != "/scalar/child" {
t.Fatalf("diagnostics = %#v, want only the invalid descendant path", diagnostics)
}
}
func TestVersionRejectedOverridePathDoesNotCauseOverlap(t *testing.T) {
type config struct {
Nested map[string]any `yaml:"nested"`
}
base := Bytes("override-version-overlap.yml", []byte("nested: {}\n"))
version := &VersionRule{Path: Keys("nested", "locked"), Check: func(Version, *Validation) error { return nil }}
_, err := testLoad(base, Options[config]{
Definition: Definition[config]{Version: version},
Overrides: []Override{
{Path: Keys("nested"), Value: map[string]any{}, Flag: "--nested"},
{Path: Keys("nested", "other"), Value: "allowed", Flag: "--other"},
},
})
if err == nil {
t.Fatal("load succeeded despite an override of a schema-version ancestor")
}
var loadErr *LoadError
if !errors.As(err, &loadErr) {
t.Fatalf("error = %T %v, want *LoadError", err, err)
}
diagnostics := loadErr.Diagnostics()
if len(diagnostics) != 1 || diagnostics[0].Code != CodeOverrideVersion || diagnostics[0].Path.String() != "/nested" {
t.Fatalf("diagnostics = %#v, want only the protected ancestor path", diagnostics)
}
}
func TestEscapedOverrideKeysStayDistinctFromMultipleSegments(t *testing.T) {
type config struct {
Nested map[string]any `yaml:"nested"`
}
base := Bytes("override-escaped-keys.yml", []byte("nested: {}\n"))
_, err := testLoad(base, Options[config]{Overrides: []Override{
{Path: Keys("nested", "a/b"), Value: "single key", Flag: "--single"},
{Path: Keys("nested", "a", "b"), Value: "nested key", Flag: "--nested"},
}})
if err != nil {
t.Fatalf("load rejected distinct escaped path tokens: %v", err)
}
}
func TestOverrideCannotReplaceSchemaVersionAncestor(t *testing.T) {
type schemaConfig struct {
Version string `yaml:"version"`
}
type config struct {
Schema schemaConfig `yaml:"schema"`
}
_, err := testLoad(Bytes("version.yml", []byte("schema: {version: 4}\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 := testLoad(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 := testLoad(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 := testLoad(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 TestAffectedOverrideOriginsSortEscapedPointers(t *testing.T) {
existingInput := parseCompositionDocument(t, "source-1", `values:
z: old
a/b: slash
a~b: tilde
nested: {x: old}
`)
existing, diagnostics := composeDocuments(existingInput)
if len(diagnostics) != 0 {
t.Fatal(diagnostics)
}
incoming := parseCompositionDocument(t, "source-2", `values:
z: new
a/b: new
a~b: new
nested: {x: new}
`)
existingValues, ok := documentAtPath(existing.root, Keys("values"))
if !ok {
t.Fatal("existing values mapping is missing")
}
incomingValues, ok := documentAtPath(incoming, Keys("values"))
if !ok {
t.Fatal("incoming values mapping is missing")
}
displaced := affectedOverrideOrigins(existingValues, incomingValues, existing.origins, Keys("values"), &mappingIndex{})
want := []string{"/values", "/values/a~0b", "/values/a~1b", "/values/nested", "/values/nested/x", "/values/z"}
if len(displaced) != len(want) {
t.Fatalf("displaced definitions = %d, want %d", len(displaced), len(want))
}
for i, definition := range displaced {
if got := definition.path.String(); got != want[i] {
t.Fatalf("displaced[%d] path = %q, want %q", i, got, want[i])
}
}
}
func TestOverrideAggregateLimitsFailBeforeApplication(t *testing.T) {
base, diagnostics := composeDocuments(&document.Node{Kind: document.Mapping})
if len(diagnostics) != 0 {
t.Fatal(diagnostics)
}
base.inputNodes = 1
_, diagnostics = preparePostStructuralOverrides(overridePreparationInput{
root: base.root, inputBytes: base.inputBytes, inputNodes: base.inputNodes,
}, nil, []Override{{Path: Keys("value"), Value: "x", Flag: "--value"}}, nil, importLimits{sources: 1, bytes: 1, nodes: 2})
if len(diagnostics) != 1 || diagnostics[0].Code != CodeOverrideLimitExceeded || len(base.root.Mapping) != 0 {
t.Fatalf("bounded preparation = %#v; base %#v", diagnostics, base.root)
}
}
func TestPreparedOverrideBatchDetachesInputsAndAllocatesSourcesInOrder(t *testing.T) {
base, diagnostics := composeDocuments(parseCompositionDocument(t, "base", "settings: {}\n"))
if len(diagnostics) != 0 {
t.Fatal(diagnostics)
}
sources := []SourceInfo{{ID: "source-1", Label: "base", Layer: LayerBase, Kind: SourceKindMapping}}
firstValue := map[string]any{"nested": map[string]any{"name": "original"}}
secondValue := []any{map[string]any{"enabled": true}}
batch, diagnostics := preparePostStructuralOverrides(overridePreparationInput{
root: base.root, inputBytes: base.inputBytes, inputNodes: base.inputNodes,
}, sources, []Override{
{Path: Keys("settings", "first"), Value: firstValue, Flag: "--first"},
{Path: Keys("settings", "second"), Value: secondValue, Flag: "--second"},
}, nil, defaultImportLimits())
if len(diagnostics) != 0 {
t.Fatalf("prepare post-structural overrides: %#v", diagnostics)
}
if len(batch.overrides) != 2 || len(batch.sources) != 3 {
t.Fatalf("prepared counts = %d overrides, %d sources", len(batch.overrides), len(batch.sources))
}
for index, want := range []string{"source-2", "source-3"} {
if batch.overrides[index].source.ID != want || batch.sources[index+1].ID != want {
t.Errorf("prepared source %d = %#v / %#v, want %s", index, batch.overrides[index].source, batch.sources[index+1], want)
}
}
if batch.sources[1].Label != "--first" || batch.sources[2].Label != "--second" {
t.Fatalf("synthetic source order = %#v", batch.sources)
}
firstValue["nested"].(map[string]any)["name"] = "changed after preparation"
preparedName, found := documentAtPath(batch.overrides[0].value, Keys("nested", "name"))
if !found || preparedName.Text != "original" {
t.Fatalf("prepared override aliases input value: %#v, %v", preparedName, found)
}
preparedName.Text = "changed prepared snapshot"
if firstValue["nested"].(map[string]any)["name"] != "changed after preparation" {
t.Fatal("prepared override mutation changed caller input")
}
batch.sources[0].Label = "changed batch source"
if sources[0].Label != "base" {
t.Fatal("prepared source list aliases the input source list")
}
if len(base.root.Mapping) != 1 || base.root.Mapping[0].Value.Kind != document.Mapping || len(base.root.Mapping[0].Value.Mapping) != 0 {
t.Fatalf("preparation mutated assembled input: %#v", base.root)
}
}
func TestPostStructuralOverridePreparationRejectsInheritanceDirectives(t *testing.T) {
base, diagnostics := composeDocuments(parseCompositionDocument(t, "base", "child: {}\nitems: []\n"))
if len(diagnostics) != 0 {
t.Fatal(diagnostics)
}
tests := []struct {
name string
override Override
path string
}{
{
name: "supplied path",
override: Override{Path: Keys("child", "extends"), Value: "parent", Flag: "--extends"},
path: "/child/extends",
},
{
name: "nested value",
override: Override{Path: Keys("items"), Value: []any{map[string]any{"extends": "parent"}}, Flag: "--items"},
path: "/items/0/extends",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, diagnostics := preparePostStructuralOverrides(overridePreparationInput{
root: base.root, inputBytes: base.inputBytes, inputNodes: base.inputNodes,
}, nil, []Override{test.override}, nil, defaultImportLimits())
if len(diagnostics) != 1 || diagnostics[0].Code != CodeOverrideDirective || diagnostics[0].Path.String() != test.path {
t.Fatalf("post-structural diagnostics = %#v, want directive at %s", diagnostics, test.path)
}
})
}
}
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()
}
func TestApplyDecodedOverridesUsesSchemaAndPreservesTypedOwnership(t *testing.T) {
t.Parallel()
type child struct {
Name string `yaml:"name"`
Enabled bool `yaml:"enabled"`
}
type config struct {
Nested child `yaml:"nested"`
Labels map[string]child `yaml:"labels"`
Items []child `yaml:"items"`
Empty []string `yaml:"empty"`
Count int `yaml:"count"`
Enabled bool `yaml:"enabled"`
Label string `yaml:"label"`
Small namedSmall `yaml:"small"`
Text validatedText `yaml:"text"`
Delay time.Duration `yaml:"delay"`
APIKey Secret `yaml:"api_key"`
Maybe *string `yaml:"maybe"`
}
defaultMaybe := "default"
defaults := config{
Nested: child{Name: "default", Enabled: true},
Labels: map[string]child{"default": {Name: "default", Enabled: true}},
Items: []child{{Name: "default", Enabled: true}},
Empty: []string{"default"},
Count: 8, Enabled: true, Label: "default", Small: 7, Text: "default", Delay: time.Second,
Maybe: &defaultMaybe,
}
input := parseDocumentForDecode(t, "nested: {name: source, enabled: true}\nlabels: {old: {name: source, enabled: true}}\nitems: [{name: discarded, enabled: true}]\nempty: [source]\ncount: 7\nenabled: true\nlabel: source\nsmall: 3\ntext: source\ndelay: 1s\napi_key: SOURCE_SECRET\nmaybe: source\n")
schema, schemaIssue := buildSchema(reflect.TypeFor[config](), testSecretBinding())
if schemaIssue != nil {
t.Fatalf("buildSchema() issue = %#v", schemaIssue)
}
decoded, decodeIssues := decodeConfiguration(schema, reflect.ValueOf(defaults), input)
if len(decodeIssues) != 0 {
t.Fatalf("decodeConfiguration() issues = %#v", decodeIssues)
}
batch, diagnostics := preparePostStructuralOverrides(overridePreparationInput{root: input}, nil, []Override{
{Path: Keys("nested"), Value: map[string]any{"enabled": false}, Flag: "--nested"},
{Path: Keys("labels"), Value: map[string]any{
"old": map[string]any{"enabled": false}, "new": map[string]any{"name": "cli"},
}, Flag: "--labels"},
{Path: Keys("items"), Value: []any{map[string]any{"name": "replacement"}}, Flag: "--items"},
{Path: Keys("empty"), Value: []any{}, Flag: "--empty"},
{Path: Keys("count"), Value: 0, Flag: "--count"},
{Path: Keys("enabled"), Value: false, Flag: "--enabled"},
{Path: Keys("label"), Value: "", Flag: "--label"},
{Path: Keys("small"), Value: 0, Flag: "--small"},
{Path: Keys("text"), Value: "cli text", Flag: "--text"},
{Path: Keys("delay"), Value: "250ms", Flag: "--delay"},
{Path: Keys("api_key"), Value: "CLI_SECRET", Flag: "--api-key"},
{Path: Keys("maybe"), Value: nil, Flag: "--maybe"},
}, nil, defaultImportLimits())
if len(diagnostics) != 0 {
t.Fatalf("preparePostStructuralOverrides() diagnostics = %#v", diagnostics)
}
result, issues := applyDecodedOverrides(schema, decoded, batch, decodeContext{})
if len(issues) != 0 {
t.Fatalf("applyDecodedOverrides() issues = %#v", issues)
}
got := result.value.Interface().(config)
if got.Nested != (child{Name: "source", Enabled: false}) || !reflect.DeepEqual(got.Labels, map[string]child{
"default": {Name: "default", Enabled: true},
"old": {Name: "source", Enabled: false},
"new": {Name: "cli"},
}) || !reflect.DeepEqual(got.Items, []child{{Name: "replacement"}}) || got.Count != 0 || got.Enabled ||
got.Empty == nil || len(got.Empty) != 0 || got.Label != "" || got.Small != 0 ||
got.Text != "cli text" || got.Delay != 250*time.Millisecond || got.APIKey.name != "CLI_SECRET" || got.Maybe != nil {
t.Fatalf("post-decode configuration = %#v", got)
}
if result.preCLI == result.final || !reflect.DeepEqual(result.preCLI, decoded.final) || len(result.overrides) != len(batch.overrides) {
t.Fatalf("result snapshots/contributions = pre:%p final:%p overrides:%d", result.preCLI, result.final, len(result.overrides))
}
if _, explicit := result.presence["/items/0/enabled"]; explicit {
t.Fatalf("replacement list inherited presence from its discarded item: %#v", result.presence)
}
if result.presence["/items/0/name"].location.SourceID != batch.overrides[2].source.ID ||
result.presence["/nested"].location.SourceID != "source-1" ||
result.origins["/items/0/name"].Layer != LayerCLI || result.origins["/nested/name"].Layer != LayerBase ||
result.origins["/nested/enabled"].Layer != LayerCLI || result.origins["/labels/default/name"].Layer != LayerDefault {
t.Fatalf("presence/origins = %#v / %#v", result.presence, result.origins)
}
if node, ok := documentAtPath(result.final, Keys("items").Index(0).Key("enabled")); !ok || node.Kind != document.Boolean || node.Text != "false" {
t.Fatalf("replacement list default projection = %#v", node)
}
got.Labels["new"] = child{Name: "mutated"}
result.overrides[0].value.Mapping[0].Value.Text = "changed"
if decoded.value.Interface().(config).Labels["old"].Name != "source" || batch.overrides[0].value.Mapping[0].Value.Text != "false" {
t.Fatal("post-decode result mutation reached the pre-CLI value or prepared batch")
}
}
func TestApplyDecodedOverridesRejectsSchemaErrorsWithoutPartialResult(t *testing.T) {
t.Parallel()
type config struct {
Nested struct {
Kept int `yaml:"kept"`
} `yaml:"nested"`
Count int `yaml:"count"`
}
input := parseDocumentForDecode(t, "nested: {kept: 1}\ncount: 2\n")
schema := mustSchemaFor[config](t)
decoded, issues := decodeConfiguration(schema, reflect.ValueOf(config{}), input)
if len(issues) != 0 {
t.Fatalf("decodeConfiguration() issues = %#v", issues)
}
for _, test := range []struct {
name string
override Override
code Code
}{
{name: "unknown nested field", override: Override{Path: Keys("nested"), Value: map[string]any{"unknown": 1}, Flag: "--nested"}, code: CodeDecodeUnknownField},
{name: "invalid type", override: Override{Path: Keys("count"), Value: "not an integer", Flag: "--count"}, code: CodeDecodeType},
} {
t.Run(test.name, func(t *testing.T) {
batch, diagnostics := preparePostStructuralOverrides(overridePreparationInput{root: input}, nil, []Override{test.override}, nil, defaultImportLimits())
if len(diagnostics) != 0 {
t.Fatalf("preparePostStructuralOverrides() diagnostics = %#v", diagnostics)
}
result, issues := applyDecodedOverrides(schema, decoded, batch, decodeContext{})
if len(issues) == 0 || issues[0].code != test.code {
t.Fatalf("decode issues = %#v, want code %q", issues, test.code)
}
if result.value.IsValid() || result.final != nil {
t.Fatalf("failed assignment returned a partial result: %#v", result)
}
original := decoded.value.Interface().(config)
if original.Count != 2 || original.Nested.Kept != 1 {
t.Fatal("failed assignment mutated the pre-CLI decoded value")
}
})
}
}