package configkit_test import ( "bytes" "io/fs" "reflect" "strings" "testing" "testing/fstest" configkit "gitea.maximumdirect.net/eric/ConfigKit" ) type inheritedEntry struct { Enabled bool `yaml:"enabled"` Count int `yaml:"count"` Resource string `yaml:"resource"` Settings map[string]map[string]string `yaml:"settings"` } type inheritedConfiguration struct { Version string `yaml:"version"` Entries map[string]inheritedEntry `yaml:"entries"` } func TestFacadeInheritancePreservesPresenceOriginsAndHistory(t *testing.T) { input := []byte(`entries: parent: enabled: false count: 0 settings: primary: {value: original} secondary: {extends: primary} child: extends: parent settings: primary: {value: custom} `) defaultRan := false result, err := configkit.Load(configkit.Bytes("config.yml", input), configkit.Options[inheritedConfiguration]{Definition: configkit.Definition[inheritedConfiguration]{ Resolve: func(config *inheritedConfiguration, resolution *configkit.Resolution) error { if !resolution.IsExplicit(configkit.Keys("entries", "child", "enabled")) { t.Error("inherited false is not explicit") } info, found := resolution.InputInfo(configkit.Keys("entries", "child", "enabled")) if !found || len(info.Origin.Inheritance) != 1 { t.Errorf("resolver inherited origin = %#v, %v", info, found) } child := config.Entries["child"] configkit.Default(resolution, configkit.Keys("entries", "child", "enabled"), &child.Enabled, true, "use fallback") defaultRan = child.Enabled config.Entries["child"] = child return nil }, }}) if err != nil { t.Fatal(err) } child := result.Config.Entries["child"] if defaultRan || child.Enabled || child.Count != 0 || child.Settings["secondary"]["value"] != "custom" { t.Fatalf("inherited values = %#v, default ran %v", child, defaultRan) } path := configkit.Keys("entries", "child", "settings", "secondary", "value") info, found := result.Metadata().Lookup(path) if !found || !info.Explicit || len(info.Origin.Inheritance) != 1 || info.Origin.Inheritance[0].From.String() != "/entries/child/settings/primary/value" || info.Origin.Inheritance[0].To.String() != path.String() { t.Fatalf("nested destination origin = %#v, %v", info, found) } info.Origin.Inheritance[0].Locations[0].SourceID = "mutated" info, _ = result.Metadata().Lookup(path) if info.Origin.Inheritance[0].Locations[0].SourceID == "mutated" { t.Fatal("public inheritance record aliases inspection state") } if _, found := result.Metadata().Lookup(configkit.Keys("entries", "child", "extends")); found { t.Fatal("consumed directive remained effective") } control, found := result.Explain(configkit.Keys("entries", "child", "extends")) if !found || control.Effective.Present || len(control.History) < 2 || control.History[len(control.History)-1].Origin.Operation != configkit.ChangeInheritance { t.Fatalf("consumed directive = %#v, %v", control, found) } explanation, found := result.Explain(path) if !found || explanation.Effective.YAML != "custom\n" { t.Fatalf("nested explanation = %#v, %v", explanation, found) } for _, change := range explanation.History { if change.Origin.Operation == configkit.ChangeDecoding { t.Fatalf("inherited value attributed to decoding: %#v", explanation.History) } } } func TestFacadeIdentifiedInheritanceResolvesChainsAndNestedCollections(t *testing.T) { const secretName = "CONFIGKIT_IDENTIFIED_INHERITANCE_SECRET" const canary = "IDENTIFIED-INHERITANCE-SECRET-CANARY" t.Setenv(secretName, canary) type step struct { ID string `yaml:"id"` Value string `yaml:"value"` } type job struct { ID string `yaml:"id"` Name string `yaml:"name"` Credential configkit.Secret `yaml:"credential"` Resource string `yaml:"resource"` Steps []step `yaml:"steps"` Settings map[string]map[string]string `yaml:"settings"` } type configuration struct { Jobs []job `yaml:"jobs"` } input := []byte(`jobs: - id: child extends: {id: parent} name: child-name - id: parent extends: {id: base} - id: base name: base-name credential: CONFIGKIT_IDENTIFIED_INHERITANCE_SECRET resource: icon.txt steps: - id: base-step value: base-step-value - id: second-step value: second-step-value settings: primary: {value: base-setting} secondary: {extends: primary} `) filesystem := configkit.NewFilesystem("identified-bundle", fstest.MapFS{ "config/identified.yml": &fstest.MapFile{Data: input}, "config/icon.txt": &fstest.MapFile{Data: []byte("identified-parent-resource")}, }) registrations := []configkit.SequenceComposition{ {Path: configkit.Keys("jobs"), IdentityField: "id"}, } result, err := configkit.Load(filesystem.File("config/identified.yml"), configkit.Options[configuration]{ Definition: configkit.Definition[configuration]{ SequenceCompositions: registrations, Secret: func(path configkit.Path) bool { return path.String() == "/jobs/0/credential" || path.String() == "/jobs/1/credential" || path.String() == "/jobs/2/credential" }, }, }) if err != nil { if loadError, ok := err.(*configkit.LoadError); ok { t.Fatalf("load identified inheritance: %v (%#v)", err, loadError.Diagnostics()) } t.Fatal(err) } if len(result.Config.Jobs) != 3 || result.Config.Jobs[0].ID != "child" || result.Config.Jobs[1].ID != "parent" || result.Config.Jobs[2].ID != "base" { t.Fatalf("inherited job positions and identities = %#v", result.Config.Jobs) } child := result.Config.Jobs[0] if child.Name != "child-name" || child.Credential.Value() != canary || child.Resource != "icon.txt" || len(child.Steps) != 2 { t.Fatalf("inherited child = %#v", child) } if got := []string{child.Steps[0].ID, child.Steps[1].ID}; strings.Join(got, ",") != "base-step,second-step" { t.Fatalf("inherited nested steps = %v", got) } if child.Steps[0].Value != "base-step-value" || child.Steps[1].Value != "second-step-value" || child.Settings["secondary"]["value"] != "base-setting" { t.Fatalf("inherited nested collection and mapping = %#v", child) } credentialPath := configkit.Keys("jobs").Index(0).Key("credential") credentialInfo, found := result.Metadata().Lookup(credentialPath) if !found || len(credentialInfo.Origin.Inheritance) != 2 { t.Fatalf("inherited secret provenance = %#v, %v", credentialInfo, found) } resourceInfo, found := result.Metadata().Lookup(configkit.Keys("jobs").Index(0).Key("resource")) if !found || len(resourceInfo.Origin.Inheritance) != 2 || len(resourceInfo.Origin.Locations) != 1 { t.Fatalf("inherited resource provenance = %#v, %v", resourceInfo, found) } context, found := result.SourceContext(resourceInfo.Origin.Locations[0].SourceID) if !found || context.BaseDir != "config" || context.FS == nil { t.Fatalf("inherited resource context = %#v, %v", context, found) } resource, readErr := fs.ReadFile(context.FS, context.BaseDir+"/"+child.Resource) if readErr != nil || string(resource) != "identified-parent-resource" { t.Fatalf("inherited resource = %q, %v", resource, readErr) } resourceHistory, found := result.Explain(configkit.Keys("jobs").Index(0).Key("resource")) if !found || len(resourceHistory.History) == 0 { t.Fatalf("inherited resource history = %#v, %v", resourceHistory, found) } credentialHistory, found := result.Explain(credentialPath) if !found || !credentialHistory.Effective.Redacted { t.Fatalf("inherited secret explanation = %#v, %v", credentialHistory, found) } identityInfo, found := result.Metadata().Lookup(configkit.Keys("jobs").Index(0).Key("id")) if !found || !identityInfo.Explicit || len(identityInfo.Origin.Inheritance) != 0 || len(identityInfo.Origin.Locations) != 1 { t.Fatalf("child identity origin and presence = %#v, %v", identityInfo, found) } if _, found := result.Metadata().Lookup(configkit.Keys("jobs").Index(0).Key("extends")); found { t.Fatal("identified record relationship remained in effective metadata") } var rendered bytes.Buffer if err := result.WriteEffective(&rendered); err != nil || strings.Contains(rendered.String(), canary) || strings.Contains(rendered.String(), "extends:") { t.Fatalf("effective inherited configuration = %q, %v", rendered.String(), err) } } func TestFacadeIdentifiedInheritanceFailureDoesNotRevealSecretCanary(t *testing.T) { const secretName = "CONFIGKIT_IDENTIFIED_INHERITANCE_FAILURE_SECRET" const canary = "IDENTIFIED-INHERITANCE-FAILURE-CANARY" t.Setenv(secretName, canary) type job struct { ID string `yaml:"id"` Credential configkit.Secret `yaml:"credential"` } type configuration struct { Jobs []job `yaml:"jobs"` } input := []byte(`jobs: - id: child extends: {id: missing} credential: CONFIGKIT_IDENTIFIED_INHERITANCE_FAILURE_SECRET `) _, err := configkit.Load(configkit.Bytes("identified-failure.yml", input), configkit.Options[configuration]{ Definition: configkit.Definition[configuration]{ SequenceCompositions: []configkit.SequenceComposition{{Path: configkit.Keys("jobs"), IdentityField: "id"}}, Secret: func(path configkit.Path) bool { return path.String() == "/jobs/0/credential" }, }, }) if err == nil || strings.Contains(err.Error(), canary) { t.Fatalf("identified inheritance failure = %v; secret canary must remain undisclosed", err) } } func TestFacadeLayeredIdentifiedInheritanceUsesSurvivingRecords(t *testing.T) { policy := func(mode string) string { return "configkit:\n sequences:\n - destination: [jobs]\n key: id\n mode: " + mode + "\n" } tests := []struct { name string lower string higher string wantCode configkit.Code wantValue string wantSuccess bool }{ { name: "upserted parent supplies its final payload", lower: policy("upsert") + "jobs: [{id: parent, value: lower}]\n", higher: policy("upsert") + "jobs: [{id: parent, value: higher}, {id: child, extends: {id: parent}}]\n", wantValue: "higher", wantSuccess: true, }, { name: "replacement removes a lower parent before lookup", lower: policy("upsert") + "jobs: [{id: parent, value: lower}]\n", higher: policy("replace") + "jobs: [{id: child, extends: {id: parent}}]\n", wantCode: configkit.CodeInheritanceMissing, }, { name: "upserted child without a directive replaces the invalid lower child", lower: policy("upsert") + "jobs: [{id: child, extends: {id: missing}, value: lower}]\n", higher: policy("upsert") + "jobs: [{id: child, value: higher}]\n", wantValue: "higher", wantSuccess: true, }, { name: "self-reference cannot resolve through a lower predecessor", lower: policy("upsert") + "jobs: [{id: parent, value: lower}]\n", higher: policy("upsert") + "jobs: [{id: parent, extends: {id: parent}, value: higher}]\n", wantCode: configkit.CodeInheritanceCycle, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { result, err := configkit.LoadLayered(configkit.Input{Source: configkit.Bytes("main.yml", []byte(test.higher))}, configkit.LayeredOptions[map[string]any]{ Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Bytes("lower.yml", []byte(test.lower))}}}}, }) if test.wantSuccess { if err != nil { t.Fatal(err) } jobs, ok := result.Config["jobs"].([]any) if !ok || len(jobs) == 0 { t.Fatalf("assembled jobs = %#v", result.Config["jobs"]) } child, ok := jobs[len(jobs)-1].(map[string]any) if !ok || child["value"] != test.wantValue { t.Fatalf("surviving record = %#v, want inherited value %q", jobs[len(jobs)-1], test.wantValue) } return } loadError, ok := err.(*configkit.LoadError) if !ok { t.Fatalf("load error = %v, want code %s", err, test.wantCode) } for _, diagnostic := range loadError.Diagnostics() { if diagnostic.Code == test.wantCode { return } } t.Fatalf("load diagnostics = %#v, want code %s", loadError.Diagnostics(), test.wantCode) }) } } func TestFacadeInheritanceLayeredPrecedenceAndSourceContext(t *testing.T) { filesystem := configkit.NewFilesystem("bundle", fstest.MapFS{ "config/main.yml": &fstest.MapFile{Data: []byte("entries: {child: {extends: parent}}\n")}, "shared/parent.yml": &fstest.MapFile{Data: []byte("entries: {parent: {resource: icon.txt, count: 1}}\n")}, "shared/flags.yml": &fstest.MapFile{Data: []byte("entries: {parent: {enabled: false}}\n")}, "shared/icon.txt": &fstest.MapFile{Data: []byte("parent-resource")}, "config/overlays/test.yml": &fstest.MapFile{Data: []byte("entries: {parent: {count: 2}}\n")}, }) result, err := configkit.LoadLayered(configkit.Input{Source: filesystem.File("config/main.yml")}, configkit.LayeredOptions[inheritedConfiguration]{ Underlays: []configkit.Underlay{ {Inputs: []configkit.Input{{Source: filesystem.File("shared/parent.yml")}}}, {Inputs: []configkit.Input{{Source: filesystem.File("shared/flags.yml")}}}, }, Overlay: "test", OverlayFilesystem: &configkit.FilesystemContext{Root: "config", BaseDir: "config", FS: &filesystem}, Overrides: []configkit.Override{{Path: configkit.Keys("entries", "child", "count"), Value: 3, Flag: "--count"}}, }) if err != nil { t.Fatal(err) } child := result.Config.Entries["child"] if child.Count != 3 || child.Resource != "icon.txt" { t.Fatalf("layered inheritance = %#v", child) } info, found := result.Metadata().Lookup(configkit.Keys("entries", "child", "resource")) if !found || len(info.Origin.Inheritance) != 1 || len(info.Origin.Locations) != 1 { t.Fatalf("resource origin = %#v, %v", info, found) } context, found := result.SourceContext(info.Origin.Locations[0].SourceID) if !found || context.BaseDir != "shared" || context.FS == nil { t.Fatalf("source context = %#v, %v", context, found) } data, err := fs.ReadFile(context.FS, context.BaseDir+"/"+child.Resource) if err != nil || string(data) != "parent-resource" { t.Fatalf("source-relative resource = %q, %v", data, err) } count, found := result.Metadata().Lookup(configkit.Keys("entries", "child", "count")) if !found || count.Origin.Layer != configkit.LayerCLI || len(count.Origin.Inheritance) != 0 { t.Fatalf("CLI replacement origin = %#v, %v", count, found) } var rendered bytes.Buffer if err := result.WriteEffective(&rendered); err != nil || strings.Contains(rendered.String(), "extends:") { t.Fatalf("effective rendering = %q, %v", rendered.String(), err) } } func TestFacadeInheritanceMappingMatchesBytes(t *testing.T) { values := map[string]any{"entries": map[string]any{ "parent": map[string]any{"enabled": false, "count": 0}, "child": map[string]any{"extends": "parent"}, }} mapped, err := configkit.Load[inheritedConfiguration](configkit.Mapping("mapping", values)) if err != nil { t.Fatal(err) } fromYAML, err := configkit.Load[inheritedConfiguration](configkit.Bytes("config.yml", []byte("entries: {parent: {enabled: false, count: 0}, child: {extends: parent}}\n"))) if err != nil || !reflect.DeepEqual(mapped.Config, fromYAML.Config) { t.Fatalf("mapping and bytes differ: %#v, %#v, %v", mapped, fromYAML, err) } } func TestFacadeInheritanceProtectsVersionOwnership(t *testing.T) { for _, test := range []struct { name, input string path configkit.Path wantFailure bool }{ {"inherited version", "entries: {parent: {count: 1}, child: {extends: parent}}\n", configkit.Keys("entries", "child", "count"), true}, {"overridden version", "entries: {parent: {count: 1}, child: {extends: parent, count: 1}}\n", configkit.Keys("entries", "child", "count"), true}, {"direct unchanged version", "entries: {parent: {enabled: false}, child: {extends: parent, count: 1}}\n", configkit.Keys("entries", "child", "count"), false}, {"consumed checked directive", "entries: {parent: {count: 1}, child: {extends: parent}}\n", configkit.Keys("entries", "child", "extends"), true}, {"independent top-level version", "version: v1\nentries: {parent: {count: 1}, child: {extends: parent}}\n", configkit.Keys("version"), false}, } { t.Run(test.name, func(t *testing.T) { checks := struct{ version, defaults, resolution, validation int }{} result, err := configkit.Load(configkit.Bytes("version.yml", []byte(test.input)), configkit.Options[inheritedConfiguration]{Definition: configkit.Definition[inheritedConfiguration]{ Version: &configkit.VersionRule{Path: test.path, Check: func(configkit.Version, *configkit.Validation) error { checks.version++ return nil }}, Defaults: func() inheritedConfiguration { checks.defaults++; return inheritedConfiguration{} }, Resolve: func(*inheritedConfiguration, *configkit.Resolution) error { checks.resolution++; return nil }, Validate: func(*inheritedConfiguration, *configkit.Validation) { checks.validation++ }, }}) if test.wantFailure { if result != nil || err == nil { t.Fatalf("protected version = %#v, %v", result, err) } loadError, ok := err.(*configkit.LoadError) if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != configkit.CodeInheritanceVersion || checks != (struct{ version, defaults, resolution, validation int }{version: 1}) { t.Fatalf("version failure = %v, callbacks %#v", err, checks) } } else if err != nil || result == nil || checks != (struct{ version, defaults, resolution, validation int }{1, 1, 1, 1}) { t.Fatalf("unchanged version = %#v, %v, callbacks %#v", result, err, checks) } }) } } func TestFacadeInheritanceFailureStopsLaterCallbacks(t *testing.T) { checks := struct{ defaults, resolution, validation int }{} result, err := configkit.Load(configkit.Bytes("invalid.yml", []byte("entries: {child: {extends: PRIVATE-REFERENCE-CANARY}}\n")), configkit.Options[inheritedConfiguration]{Definition: configkit.Definition[inheritedConfiguration]{ Defaults: func() inheritedConfiguration { checks.defaults++; return inheritedConfiguration{} }, Resolve: func(*inheritedConfiguration, *configkit.Resolution) error { checks.resolution++; return nil }, Validate: func(*inheritedConfiguration, *configkit.Validation) { checks.validation++ }, }}) if result != nil || err == nil || checks != (struct{ defaults, resolution, validation int }{}) || strings.Contains(err.Error(), "PRIVATE-REFERENCE-CANARY") { t.Fatalf("invalid inheritance = %#v, %v, callbacks %#v", result, err, checks) } loadError, ok := err.(*configkit.LoadError) if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != configkit.CodeInheritanceMissing { t.Fatalf("missing reference diagnostics = %v", err) } } func TestFacadeInheritanceResolvesAfterImportsAndCLIReferenceChange(t *testing.T) { filesystem := configkit.NewFilesystem("bundle", fstest.MapFS{ "main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: parent.yml}]}\nentries: {child: {extends: first}}\n")}, "parent.yml": &fstest.MapFile{Data: []byte("entries: {first: {count: 1}, second: {count: 2}}\n")}, }) result, err := configkit.Load(filesystem.File("main.yml"), configkit.Options[inheritedConfiguration]{ Imports: true, Overrides: []configkit.Override{{Path: configkit.Keys("entries", "child", "extends"), Value: "second", Flag: "--extends"}}, }) if err != nil || result.Config.Entries["child"].Count != 2 { t.Fatalf("import and CLI reference = %#v, %v", result, err) } history := result.Metadata().History(configkit.Keys("entries", "child", "extends")) if len(history) < 3 || history[len(history)-1].Origin.Operation != configkit.ChangeInheritance { t.Fatalf("superseded reference history = %#v", history) } conflicting := configkit.NewFilesystem("bundle", fstest.MapFS{ "main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: parent.yml}]}\nentries: {child: {extends: first}, first: {count: 3}}\n")}, "parent.yml": &fstest.MapFile{Data: []byte("entries: {first: {count: 1}}\n")}, }) _, err = configkit.Load[inheritedConfiguration](conflicting.File("main.yml"), configkit.Options[inheritedConfiguration]{Imports: true}) loadError, ok := err.(*configkit.LoadError) if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != configkit.CodeComposeConflict { t.Fatalf("strict conflict before expansion = %v", err) } } func TestFacadeInheritanceSelectedOverlayCancelsRelationship(t *testing.T) { filesystem := configkit.NewFilesystem("bundle", fstest.MapFS{ "main.yml": &fstest.MapFile{Data: []byte("entries: {parent: {count: 7}, child: {extends: parent, enabled: false}}\n")}, "overlays/test.yml": &fstest.MapFile{Data: []byte("entries: {child: {extends: null}}\n")}, }) result, err := configkit.Load(filesystem.File("main.yml"), configkit.Options[inheritedConfiguration]{ Overlay: "test", Filesystem: &configkit.FilesystemContext{Root: ".", FS: &filesystem}, }) if err != nil || result.Config.Entries["child"].Count != 0 || result.Config.Entries["child"].Enabled { if loadError, ok := err.(*configkit.LoadError); ok { t.Fatalf("cancelled relationship = %#v, %v, diagnostics %#v", result, err, loadError.Diagnostics()) } t.Fatalf("cancelled relationship = %#v, %v", result, err) } history := result.Metadata().History(configkit.Keys("entries", "child", "extends")) if len(history) < 3 || history[len(history)-1].Origin.Operation != configkit.ChangeInheritance { t.Fatalf("cancellation history = %#v", history) } } func TestFacadeInheritedSecretsStayRedactedAfterLookup(t *testing.T) { const canary = "INHERITED-RELEASE-TEST-CREDENTIAL" t.Setenv("CONFIGKIT_INHERITED_TEST_SECRET", canary) type entry struct { Credential configkit.Secret `yaml:"credential"` Plain string `yaml:"plain"` } type configuration struct { Entries map[string]entry `yaml:"entries"` } result, err := configkit.Load[configuration](configkit.Bytes("secret.yml", []byte("entries: {parent: {credential: CONFIGKIT_INHERITED_TEST_SECRET, plain: visible}, child: {extends: parent}}\n")), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{ Secret: func(path configkit.Path) bool { return path.String() == "/entries/parent/credential" }, }}) if err != nil || result.Config.Entries["child"].Credential.Value() != canary { t.Fatalf("inherited credential = %#v, %v", result, err) } path := configkit.Keys("entries", "child", "credential") explanation, found := result.Explain(path) if !found || !explanation.Effective.Redacted || strings.Contains(explanation.Effective.YAML, canary) { t.Fatalf("credential explanation = %#v, %v", explanation, found) } var rendered bytes.Buffer if err := result.WriteEffective(&rendered); err != nil || strings.Contains(rendered.String(), canary) || !strings.Contains(rendered.String(), "visible") { t.Fatalf("effective secret rendering = %q, %v", rendered.String(), err) } rendered.Reset() if err := result.WriteExplanation(&rendered, path); err != nil || strings.Contains(rendered.String(), canary) { t.Fatalf("historical secret rendering = %q, %v", rendered.String(), err) } }