Files
ConfigKit/internal/engine/override_lifecycle_test.go

208 lines
7.9 KiB
Go

package engine
import (
"reflect"
"testing"
)
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)
})
}