package configkit_test import ( "bytes" "errors" "fmt" "io" "io/fs" "os" "path" "path/filepath" "reflect" "strings" "testing" "testing/fstest" configkit "gitea.maximumdirect.net/eric/ConfigKit" ) func TestFacadeInputDestinationsAcrossSourceKinds(t *testing.T) { directory := t.TempDir() hostPath := filepath.Join(directory, "host.yml") if err := os.WriteFile(hostPath, []byte("value: host\n"), 0o600); err != nil { t.Fatal(err) } native := configkit.NewFilesystem("native", fstest.MapFS{ "main.yml": &fstest.MapFile{Data: []byte("value: native\n")}, }) const literalValuePath = "/collections/weather.reporter/profiles/a~1b/~0/0" destination := configkit.Keys("collections", "weather.reporter", "profiles", "a/b", "~", "0") for _, test := range []struct { name string source configkit.Source want string kind configkit.SourceKind }{ {name: "host file", source: configkit.File(hostPath), want: "host", kind: configkit.SourceKindFile}, {name: "native filesystem", source: native.File("main.yml"), want: "native", kind: configkit.SourceKindFilesystem}, {name: "bytes", source: configkit.Bytes("bytes", []byte("value: bytes\n")), want: "bytes", kind: configkit.SourceKindBytes}, {name: "reader", source: configkit.Reader("reader", strings.NewReader("value: reader\n")), want: "reader", kind: configkit.SourceKindReader}, {name: "mapping", source: configkit.Mapping("mapping", map[string]any{"value": "mapping"}), want: "mapping", kind: configkit.SourceKindMapping}, } { t.Run(test.name, func(t *testing.T) { result, err := configkit.LoadLayered[map[string]any](configkit.Input{Source: test.source, Destination: destination}) if err != nil { t.Fatalf("LoadLayered: %v", err) } if got := facadeDestinationValue(t, result.Config, "collections", "weather.reporter", "profiles", "a/b", "~", "0", "value"); got != test.want { t.Fatalf("placed value = %#v, want %q", got, test.want) } sources := result.Sources() if len(sources) != 1 || sources[0].Kind != test.kind || sources[0].Destination.String() != destination.String() || destination.String() != literalValuePath { t.Fatalf("source placement = %#v, destination = %q", sources, destination.String()) } }) } } func TestFacadeDestinationRootEmptyMappingNoSourceAndInvalidPaths(t *testing.T) { for _, test := range []struct { name string destination configkit.Path }{ {name: "omitted destination"}, {name: "explicit empty destination", destination: configkit.Path{}}, } { t.Run(test.name, func(t *testing.T) { result, err := configkit.LoadLayered[map[string]any](configkit.Input{Source: configkit.Bytes("root", []byte("value: root\n")), Destination: test.destination}) if err != nil { t.Fatalf("root load: %v", err) } if result.Config["value"] != "root" || len(result.Sources()) != 1 || result.Sources()[0].Destination.String() != "" { t.Fatalf("root placement = %#v, sources %#v", result.Config, result.Sources()) } }) } emptyPath := configkit.Keys("collections", "empty") empty, err := configkit.LoadLayered[map[string]any](configkit.Input{ Source: configkit.Mapping("empty", map[string]any{}), Destination: emptyPath, }) if err != nil { t.Fatalf("empty mapping load: %v", err) } if value := facadeDestinationMap(t, empty.Config, "collections")["empty"]; value == nil { t.Fatal("empty mapping did not materialize its destination") } if info, ok := empty.Metadata().Lookup(emptyPath); !ok || !info.Explicit { t.Fatalf("empty destination metadata = %#v, %v", info, ok) } absentPath := configkit.Keys("collections", "absent") absent, err := configkit.LoadLayered[map[string]any](configkit.Input{Source: configkit.NoSource(), Destination: absentPath}) if err != nil { t.Fatalf("NoSource with a destination: %v", err) } if len(absent.Sources()) != 0 { t.Fatalf("NoSource created source records: %#v", absent.Sources()) } if _, ok := absent.Metadata().Lookup(absentPath); ok { t.Fatal("NoSource materialized its destination") } reader := &destinationCountingReader{} for _, test := range []struct { name string source configkit.Source destination configkit.Path wantReads int }{ {name: "index segment", source: configkit.Reader("unread", reader), destination: configkit.Keys("catalog").Index(0)}, {name: "reserved configkit key", source: configkit.NoSource(), destination: configkit.Keys("configkit")}, {name: "reserved extends key", source: configkit.NoSource(), destination: configkit.Keys("catalog", "extends")}, } { t.Run(test.name, func(t *testing.T) { _, err := configkit.LoadLayered[map[string]any](configkit.Input{Source: test.source, Destination: test.destination}) loadError, ok := err.(*configkit.LoadError) if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != configkit.CodeOptionsInvalid { t.Fatalf("invalid destination error = %#v", err) } if reader.reads != test.wantReads { t.Fatalf("reader calls = %d, want %d", reader.reads, test.wantReads) } }) } } func TestFacadeImportedSourcesRetainInputDestination(t *testing.T) { directory := t.TempDir() mainPath := filepath.Join(directory, "main.yml") fragmentPath := filepath.Join(directory, "fragment.yml") if err := os.WriteFile(mainPath, []byte("configkit: {imports: [{path: fragment.yml}]}\nmain: present\n"), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(fragmentPath, []byte("imported: present\n"), 0o600); err != nil { t.Fatal(err) } destination := configkit.Keys("collections", "weatherreporter") result, err := configkit.LoadLayered[map[string]any](configkit.Input{ Source: configkit.File(mainPath), Imports: true, Destination: destination, }) if err != nil { t.Fatalf("mounted import load: %v", err) } collection := facadeDestinationMap(t, result.Config, "collections", "weatherreporter") if collection["main"] != "present" || collection["imported"] != "present" { t.Fatalf("mounted imported values = %#v", collection) } sources := result.Sources() if len(sources) != 2 || sources[0].Destination.String() != destination.String() || sources[1].Destination.String() != destination.String() { t.Fatalf("mounted import source destinations = %#v", sources) } if sources[1].ImportedFrom == nil { t.Fatalf("imported source lacks its authored directive: %#v", sources[1]) } } func TestFacadeSameFragmentMountsAtSeparateCollectionDestinations(t *testing.T) { weatherDestination := configkit.Keys("collections", "weather", "prompts") homeDestination := configkit.Keys("collections", "homewatch", "prompts") result, err := configkit.LoadLayered( configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[map[string]any]{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{ {Source: configkit.Bytes("weather", []byte("summarize.alerts: {title: weather}\n")), Destination: weatherDestination}, {Source: configkit.Bytes("homewatch", []byte("summarize.alerts: {title: home}\n")), Destination: homeDestination}, }}}}, ) if err != nil { t.Fatalf("LoadLayered: %v", err) } for _, test := range []struct { path []string want string }{ {path: []string{"collections", "weather", "prompts", "summarize.alerts", "title"}, want: "weather"}, {path: []string{"collections", "homewatch", "prompts", "summarize.alerts", "title"}, want: "home"}, } { if got := facadeDestinationValue(t, result.Config, test.path...); got != test.want { t.Errorf("value at %v = %#v, want %q", test.path, got, test.want) } } sources := result.Sources() if len(sources) != 2 || sources[0].Destination.String() != weatherDestination.String() || sources[1].Destination.String() != homeDestination.String() { t.Fatalf("independent collection source placements = %#v", sources) } } type destinationCountingReader struct{ reads int } func (reader *destinationCountingReader) Read([]byte) (int, error) { reader.reads++ return 0, io.EOF } func TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff(t *testing.T) { t.Parallel() filesystem := configkit.NewFilesystem("resources", fstest.MapFS{ "assets/summary.md": &fstest.MapFile{Data: []byte("source-relative summary")}, }) context := &configkit.FilesystemContext{Root: ".", BaseDir: "assets", FS: &filesystem} destination := configkit.Keys("catalog", "entries") tokenPath := configkit.Keys("catalog", "entries", "item", "token") titlePath := configkit.Keys("catalog", "entries", "item", "title") var callbacks struct{ resolve, validate int } load := func(token string) *configkit.Result[map[string]any] { t.Helper() main := configkit.Input{ Source: configkit.Bytes("main", []byte("item:\n title: main\n resource: summary.md\n token: "+token+"\n")), Destination: destination, Filesystem: context, } underlay := configkit.Input{ Source: configkit.Bytes("underlay", []byte("item:\n title: lower\n retained: lower-value\n token: LOWER-TOKEN-CANARY\n")), Destination: destination, } result, err := configkit.LoadLayered(main, configkit.LayeredOptions[map[string]any]{ Underlays: []configkit.Underlay{{Inputs: []configkit.Input{underlay}}}, Definition: configkit.Definition[map[string]any]{ Resolve: func(configuration *map[string]any, resolution *configkit.Resolution) error { callbacks.resolve++ resourcePath := configkit.Keys("catalog", "entries", "item", "resource") input, ok := resolution.InputInfo(resourcePath) if !ok || len(input.Origin.Locations) != 1 { return fmt.Errorf("mounted resource input has no single source origin") } source, ok := resolution.Source(input.Origin.Locations[0].SourceID) if !ok || source.Destination.String() != destination.String() { return fmt.Errorf("resolution source did not retain its destination") } resourceContext, ok := resolution.SourceContext(source.ID) if !ok { return fmt.Errorf("resolution source context is unavailable") } data, err := fs.ReadFile(resourceContext.FS, path.Join(resourceContext.BaseDir, "summary.md")) if err != nil { return fmt.Errorf("read source-relative resource: %w", err) } entry := facadeDestinationMap(t, facadeDestinationMap(t, facadeDestinationMap(t, *configuration, "catalog"), "entries"), "item") entry["resource_text"] = string(data) resolution.Record(configkit.Keys("catalog", "entries", "item", "resource_text"), configkit.ChangeNormalization, "read source-relative summary", resourcePath) return nil }, Validate: func(*map[string]any, *configkit.Validation) { callbacks.validate++ }, Secret: func(candidate configkit.Path) bool { return candidate.String() == tokenPath.String() }, }, }) if err != nil { t.Fatalf("mounted load: %v", err) } return result } first := load("FIRST-TOKEN-CANARY") second := load("SECOND-TOKEN-CANARY") if callbacks.resolve != 2 || callbacks.validate != 2 { t.Fatalf("lifecycle callbacks = %#v", callbacks) } for _, result := range []*configkit.Result[map[string]any]{first, second} { entry := facadeDestinationMap(t, facadeDestinationMap(t, facadeDestinationMap(t, result.Config, "catalog"), "entries"), "item") if entry["title"] != "main" || entry["retained"] != "lower-value" || entry["resource_text"] != "source-relative summary" { t.Fatalf("mounted main/underlay configuration = %#v", entry) } sources := result.Sources() if len(sources) != 2 || sources[0].Destination.String() != destination.String() || sources[1].Destination.String() != destination.String() { t.Fatalf("public source destinations = %#v", sources) } resource, ok := result.SourceContext(sources[0].ID) if !ok || resource.BaseDir != "assets" { t.Fatalf("completed source context = %#v, %v", resource, ok) } data, err := fs.ReadFile(resource.FS, path.Join(resource.BaseDir, "summary.md")) if err != nil || string(data) != "source-relative summary" { t.Fatalf("completed relative resource = %q, %v", data, err) } if info, ok := result.Metadata().Lookup(titlePath); !ok || info.Origin.Layer != configkit.LayerBase { t.Fatalf("mounted title metadata = %#v, %v", info, ok) } if history := result.Metadata().History(titlePath); len(history) < 2 { t.Fatalf("mounted title history = %#v", history) } if explanation, ok := result.Explain(tokenPath); !ok || !explanation.Effective.Redacted { t.Fatalf("mounted secret explanation = %#v, %v", explanation, ok) } var effective, explanation bytes.Buffer if err := result.WriteEffective(&effective); err != nil { t.Fatal(err) } if err := result.WriteExplanation(&explanation, tokenPath); err != nil { t.Fatal(err) } output := effective.String() + explanation.String() for _, canary := range []string{"FIRST-TOKEN-CANARY", "SECOND-TOKEN-CANARY", "LOWER-TOKEN-CANARY"} { if strings.Contains(output, canary) { t.Fatalf("public inspection leaked %q: %s", canary, output) } } } // Mutating published records and query copies must not alter the retained result. returnedSources := first.Sources() returnedSources[0].Destination = configkit.Keys("mutated") if got := first.Sources()[0].Destination.String(); got != destination.String() { t.Fatalf("source destination changed after caller mutation: %q", got) } info, ok := first.Metadata().Lookup(titlePath) if !ok { t.Fatal("mounted metadata lookup failed") } info.Origin.Locations[0].SourceID = "mutated" if fresh, ok := first.Metadata().Lookup(titlePath); !ok || fresh.Origin.Locations[0].SourceID == "mutated" { t.Fatalf("metadata copy mutation escaped: %#v, %v", fresh, ok) } history := first.Metadata().History(titlePath) if len(history) == 0 { t.Fatal("mounted history is empty") } history[0].Path = configkit.Keys("mutated") if fresh := first.Metadata().History(titlePath); len(fresh) == 0 || fresh[0].Path.String() != titlePath.String() { t.Fatalf("history copy mutation escaped: %#v", fresh) } differences, err := configkit.Diff(first, second) if err != nil { t.Fatal(err) } if len(differences) != 1 || differences[0].Path.String() != tokenPath.String() || !differences[0].Left.Redacted || !differences[0].Right.Redacted || strings.Contains(differences[0].Left.YAML+differences[0].Right.YAML, "TOKEN-CANARY") { t.Fatalf("public mounted secret diff = %#v", differences) } if callbacks.resolve != 2 || callbacks.validate != 2 { t.Fatalf("inspection reran lifecycle callbacks: %#v", callbacks) } } func TestFacadeFailedLoadExposesDetachedSourceDestination(t *testing.T) { type configuration struct { Tenant struct { Count int `yaml:"count"` } `yaml:"tenant"` } destination := configkit.Keys("tenant") _, err := configkit.LoadLayered[configuration](configkit.Input{ Source: configkit.Bytes("invalid", []byte("count: not-an-integer\n")), Destination: destination, }) loadError, ok := err.(*configkit.LoadError) if !ok { t.Fatalf("failed load error = %T %v", err, err) } sources := loadError.Sources() if len(sources) != 1 || sources[0].Destination.String() != destination.String() { t.Fatalf("failed source destinations = %#v", sources) } sources[0].Destination = configkit.Keys("caller-change") if fresh := loadError.Sources(); len(fresh) != 1 || fresh[0].Destination.String() != destination.String() { t.Fatalf("failed source destination was not detached: %#v", fresh) } } func TestFacadeDestinationMappingFailurePaths(t *testing.T) { t.Parallel() cycle := map[string]any{} cycle["self"] = cycle for _, fixture := range []struct { name string values map[string]any path func(configkit.Path) configkit.Path }{ { name: "unsupported nested sequence value", values: map[string]any{"a/b": []any{map[string]any{"~": func() {}}}}, path: func(base configkit.Path) configkit.Path { return base.Key("a/b").Index(0).Key("~") }, }, { name: "cyclic value", values: map[string]any{"item": cycle}, path: func(base configkit.Path) configkit.Path { return base.Key("item").Key("self") }, }, { name: "constructor failure keeps source coordinates", path: func(configkit.Path) configkit.Path { return configkit.Path{} }, }, } { t.Run(fixture.name, func(t *testing.T) { for _, role := range []string{"root", "mounted main", "mounted underlay"} { t.Run(role, func(t *testing.T) { var destination configkit.Path if role != "root" { destination = configkit.Keys("catalog", "entries") } wantPath := fixture.path(destination) input := configkit.Input{Source: configkit.Mapping("invalid mapping", fixture.values), Destination: destination} var result *configkit.Result[map[string]any] var err error if role == "mounted underlay" { result, err = configkit.LoadLayered(configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[map[string]any]{ Underlays: []configkit.Underlay{{Inputs: []configkit.Input{input}}}, }) } else { result, err = configkit.LoadLayered[map[string]any](input) } var loadError *configkit.LoadError if result != nil || !errors.As(err, &loadError) { t.Fatalf("failed load = %#v, %v", result, err) } diagnostics := loadError.Diagnostics() if len(diagnostics) != 1 { t.Fatalf("diagnostics = %#v", diagnostics) } diagnostic := diagnostics[0] if diagnostic.Code != configkit.CodeSourceInvalid || !reflect.DeepEqual(diagnostic.Path, wantPath) { t.Fatalf("diagnostic = %#v; want source.invalid at %q", diagnostic, wantPath.String()) } if diagnostic.Primary == nil || *diagnostic.Primary != (configkit.Location{SourceID: "source-1"}) || len(diagnostic.Related) != 0 { t.Fatalf("mapping source locations = %#v, %#v", diagnostic.Primary, diagnostic.Related) } sources := loadError.Sources() if len(sources) != 1 || sources[0].ID != "source-1" || sources[0].Destination.String() != destination.String() { t.Fatalf("error sources = %#v", sources) } }) } }) } } func facadeDestinationMap(t *testing.T, value map[string]any, keys ...string) map[string]any { t.Helper() for _, key := range keys { child, ok := value[key].(map[string]any) if !ok { t.Fatalf("%q = %#v, want mapping", key, value[key]) } value = child } return value } func facadeDestinationValue(t *testing.T, value map[string]any, keys ...string) any { t.Helper() return facadeDestinationMap(t, value, keys[:len(keys)-1]...)[keys[len(keys)-1]] }