package configkit_test import ( "errors" "fmt" "io/fs" "os" "path" "path/filepath" "reflect" "strings" "testing" "testing/fstest" configkit "gitea.maximumdirect.net/eric/ConfigKit" ) type observedResourceFS struct { files fstest.MapFS opens int closed bool } func (f *observedResourceFS) Open(name string) (fs.File, error) { f.opens++ if f.closed { return nil, fs.ErrClosed } return f.files.Open(name) } type resourceFacadeConfig struct { Native string `yaml:"native"` Shared string `yaml:"shared"` Host string `yaml:"host"` Selected string `yaml:"selected"` } func TestFacadeSourceContextsFollowOriginsWithoutQueryIO(t *testing.T) { host := t.TempDir() if err := os.WriteFile(filepath.Join(host, "main.yml"), []byte("host: host.txt\n"), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(host, "host.txt"), []byte("host-v1"), 0o600); err != nil { t.Fatal(err) } borrowed := &observedResourceFS{files: fstest.MapFS{ "bundle/config/defaults.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: ../shared.yml}]}\nnative: icon.txt\n")}, "bundle/shared.yml": &fstest.MapFile{Data: []byte("shared: shared.txt\n")}, "bundle/overlays/test.yml": &fstest.MapFile{Data: []byte("selected: overlay.txt\n")}, "bundle/config/icon.txt": &fstest.MapFile{Data: []byte("icon-v1")}, "bundle/shared.txt": &fstest.MapFile{Data: []byte("shared-v1")}, "bundle/overlays/overlay.txt": &fstest.MapFile{Data: []byte("overlay-v1")}, }} namespace := configkit.NewFilesystem("bundle", borrowed) var resolverContext configkit.ResourceContext result, err := configkit.LoadLayered(configkit.Input{Source: configkit.File(filepath.Join(host, "main.yml"))}, configkit.LayeredOptions[resourceFacadeConfig]{ Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{ Source: namespace.File("bundle/config/defaults.yml"), Imports: true, Filesystem: &configkit.FilesystemContext{Root: "bundle", FS: &namespace}, }}}}, Overlay: "test", OverlayFilesystem: &configkit.FilesystemContext{Root: "bundle", BaseDir: "bundle", FS: &namespace}, Definition: configkit.Definition[resourceFacadeConfig]{Resolve: func(config *resourceFacadeConfig, resolution *configkit.Resolution) error { before := borrowed.opens var ok bool resolverContext, ok = resolution.SourceContext("source-2") if !ok || resolverContext.BaseDir != "config" || resolverContext.FS == nil { t.Errorf("resolver context = %#v, %v", resolverContext, ok) } if _, ok := resolution.SourceContext("missing"); ok || borrowed.opens != before { t.Error("resolution context query performed I/O or guessed an unknown source") } return nil }}, }) if err != nil { t.Fatalf("load: %v, diagnostics %#v", err, err.(*configkit.LoadError).Diagnostics()) } queriesBefore := borrowed.opens for _, test := range []struct { field, wantBase, wantData string }{ {"native", "config", "icon-v1"}, {"shared", ".", "shared-v1"}, {"selected", "overlays", "overlay-v1"}, {"host", ".", "host-v1"}, } { origin, ok := result.Metadata().Lookup(configkit.Keys(test.field)) if !ok || len(origin.Origin.Locations) != 1 { t.Fatalf("%s origin = %#v, %v", test.field, origin, ok) } context, ok := result.SourceContext(origin.Origin.Locations[0].SourceID) if !ok || context.FS == nil || context.BaseDir != test.wantBase { t.Fatalf("%s context = %#v, %v", test.field, context, ok) } if borrowed.opens != queriesBefore { t.Fatal("result SourceContext query performed filesystem I/O") } resourceName := path.Join(context.BaseDir, map[string]string{ "native": result.Config.Native, "shared": result.Config.Shared, "selected": result.Config.Selected, "host": result.Config.Host, }[test.field]) data, err := fs.ReadFile(context.FS, resourceName) if err != nil || string(data) != test.wantData { t.Fatalf("%s resource = %q, %v", test.field, data, err) } queriesBefore = borrowed.opens } if _, ok := result.SourceContext(""); ok { t.Fatal("empty source ID returned a context") } var nilResult *configkit.Result[resourceFacadeConfig] if _, ok := nilResult.SourceContext("source-2"); ok { t.Fatal("nil result returned a context") } if _, err := fs.ReadFile(resolverContext.FS, "../shared.txt"); !errors.Is(err, fs.ErrInvalid) { t.Fatalf("native root escape = %v", err) } borrowed.files["bundle/config/icon.txt"].Data = []byte("icon-v2") data, err := fs.ReadFile(resolverContext.FS, "config/icon.txt") if err != nil || string(data) != "icon-v2" { t.Fatalf("updated native resource = %q, %v", data, err) } borrowed.closed = true if result.Config.Native != "icon.txt" { t.Fatal("closing caller filesystem altered loaded configuration") } if _, err := fs.ReadFile(resolverContext.FS, "config/icon.txt"); !errors.Is(err, fs.ErrClosed) { t.Fatalf("closed caller filesystem error = %v", err) } } func TestFacadeHostContextSurvivesConfigurationDeletion(t *testing.T) { root := t.TempDir() main := filepath.Join(root, "main.yml") resource := filepath.Join(root, "asset.txt") if err := os.WriteFile(main, []byte("native: asset.txt\n"), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(resource, []byte("first"), 0o600); err != nil { t.Fatal(err) } result, err := configkit.Load[resourceFacadeConfig](configkit.File(main)) if err != nil { t.Fatal(err) } context, ok := result.SourceContext("source-1") if !ok || context.BaseDir != "." { t.Fatalf("host context = %#v, %v", context, ok) } if err := os.Remove(main); err != nil { t.Fatal(err) } if err := os.WriteFile(resource, []byte("second"), 0o600); err != nil { t.Fatal(err) } data, err := fs.ReadFile(context.FS, path.Join(context.BaseDir, result.Config.Native)) if err != nil || string(data) != "second" || result.Config.Native != "asset.txt" { t.Fatalf("resource after source deletion = %q, %v", data, err) } if _, err := fs.ReadFile(context.FS, "../asset.txt"); !errors.Is(err, fs.ErrInvalid) { t.Fatalf("invalid host path = %v", err) } if err := os.Remove(resource); err != nil { t.Fatal(err) } if _, err := fs.ReadFile(context.FS, "asset.txt"); !errors.Is(err, fs.ErrNotExist) { t.Fatalf("missing later resource = %v", err) } outside := filepath.Join(t.TempDir(), "outside.txt") if err := os.WriteFile(outside, []byte("outside"), 0o600); err != nil { t.Fatal(err) } if err := os.Symlink(outside, filepath.Join(root, "escape")); err == nil { if _, err := fs.ReadFile(context.FS, "escape"); !errors.Is(err, fs.ErrPermission) { t.Fatalf("host symlink escape = %v", err) } } } func TestFacadeSyntheticSourceContextsAndContextlessSources(t *testing.T) { namespace := configkit.NewFilesystem("bundle", fstest.MapFS{ "bundle/config/icon.txt": &fstest.MapFile{Data: []byte("icon")}, }) context := &configkit.FilesystemContext{Root: "bundle", BaseDir: "bundle/config", FS: &namespace} for _, test := range []struct { name string source configkit.Source }{ {"bytes", configkit.Bytes("memory", []byte("native: icon.txt\n"))}, {"reader", configkit.Reader("stream", strings.NewReader("native: icon.txt\n"))}, {"mapping", configkit.Mapping("values", map[string]any{"native": "icon.txt"})}, } { t.Run(test.name, func(t *testing.T) { result, err := configkit.Load(test.source, configkit.Options[resourceFacadeConfig]{Filesystem: context}) if err != nil { t.Fatal(err) } resource, ok := result.SourceContext("source-1") if !ok || resource.BaseDir != "config" { t.Fatalf("synthetic context = %#v, %v", resource, ok) } data, err := fs.ReadFile(resource.FS, path.Join(resource.BaseDir, result.Config.Native)) if err != nil || string(data) != "icon" { t.Fatalf("synthetic resource = %q, %v", data, err) } }) } result, err := configkit.Load[resourceFacadeConfig](configkit.Bytes("plain", []byte("native: none\n"))) if err != nil { t.Fatal(err) } if _, ok := result.SourceContext("source-1"); ok { t.Fatal("contextless byte source returned a context") } result, err = configkit.Load(configkit.NoSource(), configkit.Options[resourceFacadeConfig]{Overrides: []configkit.Override{{Path: configkit.Keys("native"), Value: "from-cli", Flag: "--native"}}}) if err != nil { t.Fatal(err) } if _, ok := result.SourceContext("source-1"); ok { t.Fatal("CLI source returned a context") } } func TestFacadeRelativeImportDestinationsPreserveNativeResourceContexts(t *testing.T) { borrowed := &observedResourceFS{files: fstest.MapFS{ "bundle/config/main.yml": &fstest.MapFile{Data: []byte("configkit:\n imports:\n - path: entries/parent.yml\n destination: [logical]\n")}, "bundle/config/entries/parent.yml": &fstest.MapFile{Data: []byte("configkit:\n imports:\n - path: nested/details.yml\n destination: [details]\nresource: icon.txt\n")}, "bundle/config/entries/nested/details.yml": &fstest.MapFile{Data: []byte("resource: detail.txt\n")}, "bundle/config/entries/icon.txt": &fstest.MapFile{Data: []byte("parent-resource")}, "bundle/config/entries/nested/detail.txt": &fstest.MapFile{Data: []byte("nested-resource")}, }} namespace := configkit.NewFilesystem("bundle", borrowed) input := configkit.Input{ Source: namespace.File("bundle/config/main.yml"), Imports: true, Filesystem: &configkit.FilesystemContext{Root: "bundle", FS: &namespace}, } load := func() *configkit.Result[map[string]any] { t.Helper() result, err := configkit.LoadLayered[map[string]any](input) if err != nil { t.Fatalf("load relative native imports: %v", err) } return result } result := load() wantDestinations := []string{configkit.Keys().String(), "/logical", "/logical/details"} wantPaths := []string{"bundle/config/main.yml", "bundle/config/entries/parent.yml", "bundle/config/entries/nested/details.yml"} sources := result.Sources() if len(sources) != len(wantPaths) { t.Fatalf("sources = %#v", sources) } for index, source := range sources { if source.ID != fmt.Sprintf("source-%d", index+1) || source.Kind != configkit.SourceKindFilesystem || source.Path != wantPaths[index] || source.Destination.String() != wantDestinations[index] { t.Errorf("source %d = %#v", index, source) } if index == 0 { if source.ImportedFrom != nil { t.Errorf("entry source has import location %#v", source.ImportedFrom) } } else if source.ImportedFrom == nil || source.ImportedFrom.SourceID != fmt.Sprintf("source-%d", index) || source.ImportedFrom.Line == 0 || source.ImportedFrom.Column == 0 { t.Errorf("source %d import location = %#v", index, source.ImportedFrom) } } for _, test := range []struct { field, effectivePath, wantBase, wantResource, wantContents string }{ {"parent resource", "/logical/resource", "config/entries", "icon.txt", "parent-resource"}, {"nested resource", "/logical/details/resource", "config/entries/nested", "detail.txt", "nested-resource"}, } { info, ok := result.Metadata().Lookup(configkit.Keys(strings.Split(strings.Trim(test.effectivePath, "/"), "/")...)) if !ok || len(info.Origin.Locations) != 1 { t.Fatalf("%s origin = %#v, %v", test.field, info, ok) } context, ok := result.SourceContext(info.Origin.Locations[0].SourceID) if !ok || context.BaseDir != test.wantBase || context.FS == nil { t.Fatalf("%s context = %#v, %v", test.field, context, ok) } contents, err := fs.ReadFile(context.FS, path.Join(context.BaseDir, test.wantResource)) if err != nil || string(contents) != test.wantContents { t.Fatalf("%s opened resource = %q, %v", test.field, contents, err) } } sources[1].Destination = configkit.Keys("caller", "mutation") for query := 0; query < 2; query++ { fresh := result.Sources() if fresh[1].Destination.String() != "/logical" { t.Fatalf("query %d source destination = %q", query, fresh[1].Destination.String()) } } second := load() if !reflect.DeepEqual(second.Sources(), result.Sources()) { t.Fatalf("independent source records differ: %#v vs %#v", second.Sources(), result.Sources()) } }