276 lines
11 KiB
Go
276 lines
11 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"`
|
|
Enabled bool `yaml:"enabled"`
|
|
Derived int `yaml:"derived"`
|
|
}
|
|
calls := 0
|
|
result, err := testLoad(NoSource(), Options[config]{
|
|
Definition: Definition[config]{
|
|
Defaults: func() config {
|
|
calls++
|
|
return config{Count: 2, Enabled: true}
|
|
},
|
|
Resolve: func(value *config, resolution *Resolution) error {
|
|
for _, key := range []string{"count", "enabled"} {
|
|
info, ok := resolution.InputInfo(Keys(key))
|
|
if !ok || !info.Explicit || info.Origin.Layer != LayerCLI {
|
|
t.Errorf("resolution %s info = %#v, %v", key, info, ok)
|
|
}
|
|
}
|
|
Default(resolution, Keys("count"), &value.Count, 10, "preserve CLI value")
|
|
Default(resolution, Keys("enabled"), &value.Enabled, true, "preserve CLI value")
|
|
Default(resolution, Keys("derived"), &value.Derived, value.Count*3, "derive from CLI count")
|
|
return nil
|
|
},
|
|
},
|
|
Overrides: []Override{
|
|
{Path: Keys("count"), Value: 0, Flag: "--count"},
|
|
{Path: Keys("enabled"), Value: false, Flag: "--enabled"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if calls != 1 || result.Config.Count != 0 || result.Config.Enabled || result.Config.Derived != 0 {
|
|
t.Fatalf("defaults-only CLI result = %#v, defaults calls %d", result.Config, calls)
|
|
}
|
|
if sources := result.Sources(); len(sources) != 2 || sources[0].Layer != LayerCLI || sources[1].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)
|
|
})
|
|
}
|