Files
ConfigKit/internal/engine/override_lifecycle_test.go

269 lines
10 KiB
Go

package engine
import (
"reflect"
"testing"
)
func TestNestedCLIOverridePreservesRetainedInputPresence(t *testing.T) {
t.Parallel()
type nested struct {
Changed int `yaml:"changed"`
Count int `yaml:"count"`
Zero int `yaml:"zero"`
Enabled bool `yaml:"enabled"`
}
type settings struct {
Nested nested `yaml:"nested"`
}
type config struct {
Settings settings `yaml:"settings"`
}
for _, layered := range []bool{false, true} {
name := "ordinary"
if layered {
name = "layered"
}
t.Run(name, func(t *testing.T) {
source := Bytes("input", []byte("settings:\n nested:\n changed: 1\n count: 2\n zero: 0\n enabled: false\n"))
definition := Definition[config]{Resolve: func(value *config, resolution *Resolution) error {
for _, key := range []string{"count", "zero", "enabled"} {
info, ok := resolution.InputInfo(Keys("settings", "nested", key))
if !ok || !info.Explicit || info.Origin.Layer != LayerBase {
t.Errorf("retained %s input info = %#v, %v", key, info, ok)
}
}
Default(resolution, Keys("settings", "nested", "count"), &value.Settings.Nested.Count, 99, "fill missing count")
Default(resolution, Keys("settings", "nested", "zero"), &value.Settings.Nested.Zero, 99, "fill missing zero")
Default(resolution, Keys("settings", "nested", "enabled"), &value.Settings.Nested.Enabled, true, "fill missing enabled")
return nil
}}
overrides := []Override{{Path: Keys("settings"), Value: map[string]any{"nested": map[string]any{"changed": 3}}, Flag: "--settings"}}
var result *testResult[config]
var err error
if layered {
result, err = testGroupedLoad(loadRequest[config]{main: loadInput{source: source}, definition: definition, overrides: overrides})
} else {
result, err = testLoad(source, Options[config]{Definition: definition, Overrides: overrides})
}
if err != nil {
t.Fatal(err)
}
if got, want := result.Config.Settings.Nested, (nested{Changed: 3, Count: 2}); got != want {
t.Fatalf("resolved nested values = %#v, want %#v", got, want)
}
for _, key := range []string{"count", "zero", "enabled"} {
info, ok := result.Metadata().Lookup(Keys("settings", "nested", key))
if !ok || !info.Explicit || info.Origin.Layer != LayerBase || len(info.Origin.Locations) != 1 || info.Origin.Locations[0].SourceID != "source-1" {
t.Errorf("retained %s metadata = %#v, %v", key, info, ok)
}
}
info, ok := result.Metadata().Lookup(Keys("settings", "nested", "changed"))
if !ok || !info.Explicit || info.Origin.Layer != LayerCLI {
t.Errorf("changed metadata = %#v, %v", info, ok)
}
})
}
}
func TestPostDecodeOverrideLifecycleMatchesOrdinaryAndLayeredLoads(t *testing.T) {
t.Parallel()
type config struct {
Count int `yaml:"count"`
Derived int `yaml:"derived"`
}
input := Bytes("settings.yml", []byte("count: 4\n"))
defaultsCalls := 0
definition := Definition[config]{
Defaults: func() config {
defaultsCalls++
return config{Count: 9, Derived: 1}
},
Resolve: func(value *config, resolution *Resolution) error {
info, ok := resolution.InputInfo(Keys("count"))
if !ok || !info.Explicit || info.Origin.Layer != LayerCLI {
t.Errorf("resolution count info = %#v, %v", info, ok)
}
if source, ok := resolution.Source("source-2"); !ok || source.Layer != LayerCLI {
t.Errorf("resolution CLI source = %#v, %v", source, ok)
}
Default(resolution, Keys("count"), &value.Count, 99, "preserve explicit count")
Default(resolution, Keys("derived"), &value.Derived, value.Count*2, "derive from CLI count", Keys("count"))
Normalize(resolution, Keys("count"), &value.Count, value.Count+1, "normalize after CLI")
return nil
},
}
options := Options[config]{Definition: definition, Overrides: []Override{{Path: Keys("count"), Value: 4, Flag: "--count"}}}
ordinary, err := testLoad(input, options)
if err != nil {
t.Fatal(err)
}
layered, err := testGroupedLoad(loadRequest[config]{
main: loadInput{source: input}, definition: definition,
overrides: []Override{{Path: Keys("count"), Value: 4, Flag: "--count"}},
})
if err != nil {
t.Fatal(err)
}
if defaultsCalls != 2 {
t.Fatalf("defaults factory calls = %d, want once per load", defaultsCalls)
}
for name, result := range map[string]*testResult[config]{"ordinary": ordinary, "layered": layered} {
if result.Config.Count != 5 || result.Config.Derived != 8 {
t.Errorf("%s resolved configuration = %#v", name, result.Config)
}
if got := historyLayers(result.Metadata().History(Keys("count"))); !reflect.DeepEqual(got, []Layer{LayerDefault, LayerBase, LayerCLI, LayerResolution}) {
t.Errorf("%s count history layers = %#v", name, got)
}
info, ok := result.Metadata().Lookup(Keys("count"))
if !ok || info.Origin.Layer != LayerResolution {
t.Errorf("%s final count origin = %#v, %v", name, info, ok)
}
}
if !reflect.DeepEqual(ordinary.Config, layered.Config) ||
!reflect.DeepEqual(ordinary.Metadata().History(Keys("count")), layered.Metadata().History(Keys("count"))) {
t.Fatalf("ordinary and layered results differ: %#v / %#v", ordinary.Config, layered.Config)
}
}
func TestNoSourceOverridesReachResolutionAfterDefaults(t *testing.T) {
t.Parallel()
type config struct {
Count int `yaml:"count"`
Derived int `yaml:"derived"`
}
calls := 0
result, err := testLoad(NoSource(), Options[config]{
Definition: Definition[config]{
Defaults: func() config {
calls++
return config{Count: 2}
},
Resolve: func(value *config, resolution *Resolution) error {
info, ok := resolution.InputInfo(Keys("count"))
if !ok || !info.Explicit || info.Origin.Layer != LayerCLI {
t.Errorf("resolution count info = %#v, %v", info, ok)
}
Default(resolution, Keys("count"), &value.Count, 10, "preserve CLI value")
Default(resolution, Keys("derived"), &value.Derived, value.Count*3, "derive from CLI count")
return nil
},
},
Overrides: []Override{{Path: Keys("count"), Value: 4, Flag: "--count"}},
})
if err != nil {
t.Fatal(err)
}
if calls != 1 || result.Config.Count != 4 || result.Config.Derived != 12 {
t.Fatalf("defaults-only CLI result = %#v, defaults calls %d", result.Config, calls)
}
if sources := result.Sources(); len(sources) != 1 || sources[0].Layer != LayerCLI {
t.Fatalf("defaults-only CLI sources = %#v", sources)
}
}
func TestCLISequenceAssignmentReplacesAppendAndUpsertResults(t *testing.T) {
t.Parallel()
type item struct {
ID string `yaml:"id"`
}
type config struct {
Items []item `yaml:"items"`
}
declaration := func(mode string, id string) Source {
return Bytes(id, []byte("configkit:\n sequences:\n - destination: [items]\n key: id\n mode: "+mode+"\nitems: [{id: "+id+"}]\n"))
}
result, err := testGroupedLoad(loadRequest[config]{
main: loadInput{source: declaration("upsert", "main")},
underlays: []loadGroup{{inputs: []loadInput{{source: declaration("append", "underlay")}}}},
overrides: []Override{{Path: Keys("items"), Value: []any{map[string]any{"id": "cli"}}, Flag: "--items"}},
})
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(result.Config.Items, []item{{ID: "cli"}}) {
t.Fatalf("CLI sequence assignment = %#v, want complete replacement", result.Config.Items)
}
}
func TestCLISecretReferenceUsesEffectiveLookupAndLocation(t *testing.T) {
t.Parallel()
type config struct {
APIKey Secret `yaml:"api_key"`
}
options := Options[config]{
Definition: Definition[config]{Defaults: func() config { return config{APIKey: SecretEnv("DEFAULT_SECRET")} }},
Overrides: []Override{{Path: Keys("api_key"), Value: "CLI_SECRET", Flag: "--api-key"}},
}
source := Bytes("secret-input", []byte("api_key: BASE_SECRET\n"))
t.Run("effective reference is looked up", func(t *testing.T) {
calls := 0
result, err := testLoadWithSecretLookup(source, func(name string) (string, bool) {
calls++
if name != "CLI_SECRET" {
t.Errorf("secret lookup name = %q", name)
}
return "effective-credential", true
}, options)
if err != nil || calls != 1 || result.Config.APIKey.Value() != "effective-credential" {
t.Fatalf("CLI secret result = %#v, calls %d, error %v", result, calls, err)
}
info, ok := result.Metadata().Lookup(Keys("api_key"))
if !ok || info.Origin.Layer != LayerCLI {
t.Fatalf("CLI secret origin = %#v, %v", info, ok)
}
})
t.Run("missing effective reference uses CLI location", func(t *testing.T) {
result, err := testLoadWithSecretLookup(source, func(name string) (string, bool) {
if name != "CLI_SECRET" {
t.Errorf("secret lookup name = %q", name)
}
return "", false
}, options)
if result != nil {
t.Fatalf("missing secret returned a result: %#v", result)
}
loadErr, ok := err.(*LoadError)
if !ok || len(loadErr.Diagnostics()) != 1 {
t.Fatalf("missing secret error = %v", err)
}
diagnostic := loadErr.Diagnostics()[0]
if diagnostic.Code != CodeSecretUnset || diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-2" {
t.Fatalf("missing secret diagnostic = %#v", diagnostic)
}
})
}
func TestCLIOverridesCannotHideEarlierFailuresOrAddInheritance(t *testing.T) {
t.Parallel()
type config struct {
Count int `yaml:"count"`
Child map[string]any `yaml:"child"`
Parent map[string]any `yaml:"parent"`
}
t.Run("decode error", func(t *testing.T) {
_, err := testLoad(Bytes("bad-count", []byte("count: invalid\n")), Options[config]{
Overrides: []Override{{Path: Keys("count"), Value: 3, Flag: "--count"}},
})
requireSecretLoadCode(t, err, CodeDecodeType)
})
t.Run("inheritance error", func(t *testing.T) {
_, err := testLoad(Bytes("missing-parent", []byte("child: {extends: missing, value: source}\n")), Options[config]{
Overrides: []Override{{Path: Keys("child"), Value: map[string]any{"value": "cli"}, Flag: "--child"}},
})
requireSecretLoadCode(t, err, CodeInheritanceMissing)
})
t.Run("cannot add relationship", func(t *testing.T) {
_, err := testLoad(Bytes("new-relationship", []byte("child: {value: source}\nparent: {value: parent}\n")), Options[config]{
Overrides: []Override{{Path: Keys("child"), Value: map[string]any{"extends": "parent"}, Flag: "--child"}},
})
requireSecretLoadCode(t, err, CodeOverrideDirective)
})
t.Run("cannot cancel relationship", func(t *testing.T) {
_, err := testLoad(Bytes("cancel-relationship", []byte("child: {extends: parent}\nparent: {value: parent}\n")), Options[config]{
Overrides: []Override{{Path: Keys("child", "extends"), Value: nil, Flag: "--extends"}},
})
requireSecretLoadCode(t, err, CodeOverrideDirective)
})
}