package configkit_test import ( "bytes" "os" "path/filepath" "reflect" "strconv" "strings" "testing" "testing/fstest" configkit "gitea.maximumdirect.net/eric/ConfigKit" ) type layeredFacadeConfig struct { Settings map[string]string `yaml:"settings"` Count int `yaml:"count"` Enabled bool `yaml:"enabled"` List []string `yaml:"list"` Optional *string `yaml:"optional"` Derived string `yaml:"derived"` } func TestFacadeLayeredMixedSourcesAndInspection(t *testing.T) { mainPath := filepath.Join(t.TempDir(), "main.yml") if err := os.WriteFile(mainPath, []byte("settings: {shared: main, secret: new-canary}\ncount: 0\nenabled: false\nlist: []\noptional: null\n"), 0o600); err != nil { t.Fatal(err) } lower := configkit.NewFilesystem("bundle", fstest.MapFS{ "main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: defaults.yml}]}\nsettings: {shared: lower, native: retained, secret: old-canary}\nlist: [lower]\n")}, "defaults.yml": &fstest.MapFile{Data: []byte("settings: {imported: retained}\n")}, "overlays/testing.yml": &fstest.MapFile{Data: []byte("settings: {from_lower_namespace: retained}\n")}, }) selected := configkit.NewFilesystem("bundle", fstest.MapFS{ "overlays/testing.yml": &fstest.MapFile{Data: []byte("settings: {shared: overlay}\n")}, }) checks := struct{ defaults, resolution, validation int }{} result, err := configkit.LoadLayered(configkit.Input{Source: configkit.File(mainPath)}, configkit.LayeredOptions[layeredFacadeConfig]{ Underlays: []configkit.Underlay{ {Inputs: []configkit.Input{ {Source: lower.File("main.yml"), Imports: true}, {Source: configkit.Mapping("programmatic-low", map[string]any{"settings": map[string]any{"mapping": "retained"}, "count": 7, "enabled": true})}, {Source: lower.File("overlays/testing.yml")}, }}, {Inputs: []configkit.Input{{Source: configkit.Mapping("programmatic-high", map[string]any{ "settings": map[string]any{"shared": "middle", "second": "retained"}, "list": []any{"middle"}, })}}}, }, Overlay: "testing", OverlayFilesystem: &configkit.FilesystemContext{Root: ".", BaseDir: ".", FS: &selected}, Overrides: []configkit.Override{{Path: configkit.Keys("settings", "shared"), Value: "cli", Flag: "--shared"}}, Definition: configkit.Definition[layeredFacadeConfig]{ Defaults: func() layeredFacadeConfig { checks.defaults++; return layeredFacadeConfig{} }, Resolve: func(config *layeredFacadeConfig, resolution *configkit.Resolution) error { checks.resolution++ if source, ok := resolution.Source("source-2"); !ok || source.Layer != configkit.LayerUnderlay || source.UnderlayIndex != 1 || source.Kind != configkit.SourceKindFilesystem { t.Errorf("resolver source = %#v, %v", source, ok) } configkit.Default(resolution, configkit.Keys("derived"), &config.Derived, config.Settings["native"], "derive from native", configkit.Keys("settings", "native")) return nil }, Validate: func(config *layeredFacadeConfig, validation *configkit.Validation) { checks.validation++ }, Secret: func(path configkit.Path) bool { return path.String() == "/settings/secret" }, }, }) if err != nil { t.Fatalf("layered load: %v, diagnostics %#v", err, err.(*configkit.LoadError).Diagnostics()) } wantSettings := map[string]string{ "shared": "cli", "secret": "new-canary", "native": "retained", "imported": "retained", "mapping": "retained", "from_lower_namespace": "retained", "second": "retained", } if !reflect.DeepEqual(result.Config.Settings, wantSettings) || result.Config.Count != 0 || result.Config.Enabled || result.Config.List == nil || len(result.Config.List) != 0 || result.Config.Optional != nil || result.Config.Derived != "retained" { t.Fatalf("configuration = %#v", result.Config) } if checks != (struct{ defaults, resolution, validation int }{1, 1, 1}) { t.Fatalf("lifecycle calls = %#v", checks) } sources := result.Sources() if len(sources) != 8 { t.Fatalf("sources = %#v", sources) } wantLayers := []configkit.Layer{configkit.LayerBase, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerUnderlay, configkit.LayerOverlay, configkit.LayerCLI} wantIndexes := []int{0, 1, 1, 1, 1, 2, 0, 0} wantKinds := []configkit.SourceKind{configkit.SourceKindFile, configkit.SourceKindFilesystem, configkit.SourceKindFilesystem, configkit.SourceKindMapping, configkit.SourceKindFilesystem, configkit.SourceKindMapping, configkit.SourceKindFilesystem, configkit.SourceKindCLI} for index, source := range sources { if source.ID != "source-"+strconv.Itoa(index+1) || source.Layer != wantLayers[index] || source.UnderlayIndex != wantIndexes[index] || source.Kind != wantKinds[index] { t.Errorf("source %d = %#v", index, source) } } if sources[4].Path != sources[6].Path || sources[4].Label != sources[6].Label { t.Fatalf("namespace-collision fixture changed: %#v, %#v", sources[4], sources[6]) } sources[1].Label = "changed" if result.Sources()[1].Label != "bundle/main.yml" { t.Fatal("source records alias retained metadata") } for _, test := range []struct { path string want configkit.Layer }{ {"/settings/native", configkit.LayerUnderlay}, {"/settings/shared", configkit.LayerCLI}, {"/derived", configkit.LayerResolution}, } { path, err := configkit.ParsePath(test.path) if err != nil { t.Fatal(err) } info, ok := result.Metadata().Lookup(path) if !ok || info.Origin.Layer != test.want { t.Errorf("%s metadata = %#v, %v", test.path, info, ok) } } container, ok := result.Metadata().Lookup(configkit.Keys("settings")) if !ok || len(container.Origin.Locations) < 4 { t.Fatalf("composite mapping origin = %#v, %v", container, ok) } explanation, ok := result.Explain(configkit.Keys("settings", "shared")) if !ok || len(explanation.History) < 5 || explanation.Origin.Layer != configkit.LayerCLI { t.Fatalf("shared history = %#v, %v", explanation, ok) } var effective, rendered bytes.Buffer if err := result.WriteEffective(&effective); err != nil { t.Fatal(err) } if err := result.WriteExplanation(&rendered, configkit.Keys("settings", "secret")); err != nil { t.Fatal(err) } if strings.Contains(effective.String()+rendered.String(), "old-canary") || strings.Contains(effective.String()+rendered.String(), "new-canary") || !strings.Contains(rendered.String(), "redacted") { t.Fatalf("secret rendering leaked: effective %q, explanation %q", effective.String(), rendered.String()) } } func TestFacadeLayeredDisplacedDescendantRemainsInspectable(t *testing.T) { type config struct { Choice string `yaml:"choice"` } result, err := configkit.LoadLayered(configkit.Input{Source: configkit.Mapping("main", map[string]any{"choice": "flat"})}, configkit.LayeredOptions[config]{ Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("lower", map[string]any{ "choice": map[string]any{"nested": "historical"}, })}}}}, }) if err != nil || result.Config.Choice != "flat" { t.Fatalf("replacement load = %#v, %v", result, err) } path := configkit.Keys("choice", "nested") explanation, ok := result.Explain(path) if !ok || explanation.Effective.Present || len(explanation.History) == 0 || explanation.History[0].Origin.Layer != configkit.LayerUnderlay { t.Fatalf("deleted descendant = %#v, %v", explanation, ok) } } func TestFacadeLayeredZeroUnderlaysMatchesOrdinaryLoad(t *testing.T) { filesystem := configkit.NewFilesystem("bundle", fstest.MapFS{ "overlays/testing.yml": &fstest.MapFile{Data: []byte("value: selected\n")}, }) context := &configkit.FilesystemContext{Root: ".", BaseDir: ".", FS: &filesystem} source := configkit.Bytes("main", []byte("value: base\n")) type config struct { Value string `yaml:"value"` } ordinary, err := configkit.Load(source, configkit.Options[config]{Overlay: "testing", Filesystem: context}) if err != nil { t.Fatal(err) } layered, err := configkit.LoadLayered(configkit.Input{Source: source}, configkit.LayeredOptions[config]{Overlay: "testing", OverlayFilesystem: context}) if err != nil { t.Fatal(err) } if ordinary.Config != layered.Config || !reflect.DeepEqual(ordinary.Sources(), layered.Sources()) { t.Fatalf("ordinary = %#v, %#v; layered = %#v, %#v", ordinary.Config, ordinary.Sources(), layered.Config, layered.Sources()) } var first, second bytes.Buffer if err := ordinary.WriteEffective(&first); err != nil { t.Fatal(err) } if err := layered.WriteEffective(&second); err != nil { t.Fatal(err) } if first.String() != second.String() { t.Fatalf("effective ordinary %q, layered %q", first.String(), second.String()) } if ordinaryExplanation, ok := ordinary.Explain(configkit.Keys("value")); !ok { t.Fatal("ordinary explanation missing") } else if layeredExplanation, ok := layered.Explain(configkit.Keys("value")); !ok || !reflect.DeepEqual(ordinaryExplanation, layeredExplanation) { t.Fatalf("explanations differ: %#v, %#v", ordinaryExplanation, layeredExplanation) } } func TestFacadeLayeredOptionAndVersionFailures(t *testing.T) { type config struct { Version string `yaml:"version"` } namespace := configkit.NewFilesystem("bundle", fstest.MapFS{ "overlays/testing.yml": &fstest.MapFile{Data: []byte("{}\n")}, }) for _, test := range []struct { name string main configkit.Input options []configkit.LayeredOptions[config] want configkit.Code }{ {"zero source", configkit.Input{}, nil, configkit.CodeSourceInvalid}, {"multiple options", configkit.Input{Source: configkit.NoSource()}, []configkit.LayeredOptions[config]{{}, {}}, configkit.CodeOptionsInvalid}, {"missing overlay context", configkit.Input{Source: configkit.NoSource()}, []configkit.LayeredOptions[config]{{Overlay: "testing"}}, configkit.CodeOptionsInvalid}, {"selected file already acquired", configkit.Input{Source: configkit.NoSource()}, []configkit.LayeredOptions[config]{{ Overlay: "testing", OverlayFilesystem: &configkit.FilesystemContext{Root: ".", BaseDir: ".", FS: &namespace}, Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: namespace.File("overlays/testing.yml")}}}}, }}, configkit.CodeOverlayRepeated}, {"underlay version", configkit.Input{Source: configkit.NoSource()}, []configkit.LayeredOptions[config]{{ Definition: configkit.Definition[config]{Version: &configkit.VersionRule{Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil }}}, Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("lower", map[string]any{"version": "forbidden"})}}}}, }}, configkit.CodeUnderlayVersion}, } { t.Run(test.name, func(t *testing.T) { _, err := configkit.LoadLayered(test.main, test.options...) loadError, ok := err.(*configkit.LoadError) if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != test.want { t.Fatalf("error = %#v, want %s", err, test.want) } }) } } func TestFacadeLayeredExplicitHostOverlayWithoutMain(t *testing.T) { root := t.TempDir() if err := os.Mkdir(filepath.Join(root, "overlays"), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(root, "overlays", "testing.yml"), []byte("value: selected\n"), 0o600); err != nil { t.Fatal(err) } type config struct { Value string `yaml:"value"` } result, err := configkit.LoadLayered(configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[config]{ Overlay: "testing", OverlayFilesystem: &configkit.FilesystemContext{Root: root, BaseDir: root}, }) if err != nil || result.Config.Value != "selected" || len(result.Sources()) != 1 || result.Sources()[0].Kind != configkit.SourceKindFile { t.Fatalf("explicit host overlay = %#v, %v", result, err) } }