689 lines
30 KiB
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 len(result.overrides) != len(batch.overrides) {
|
|
t.Fatalf("result override contributions = %d, want %d", len(result.overrides), len(batch.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")
|
|
}
|
|
})
|
|
}
|
|
}
|