package engine import ( "errors" "io/fs" "reflect" "strings" "testing" "testing/fstest" ) func TestNativeFilesystemImportsAndOverlay(t *testing.T) { filesystem := fstest.MapFS{ "config/main.yml": &fstest.MapFile{Data: []byte(`configkit: imports: - path: ../shared.yml - glob: fragments/*.yml items: {main: main} `)}, "shared.yml": &fstest.MapFile{Data: []byte("items: {shared: shared}\n")}, "config/fragments/b.yml": &fstest.MapFile{Data: []byte("items: {beta: beta}\n")}, "config/fragments/a.yml": &fstest.MapFile{Data: []byte("items: {alpha: alpha}\n")}, "overlays/testing.yml": &fstest.MapFile{Data: []byte("items: {main: overlaid}\n")}, } namespace := newNativeNamespace("bundle", filesystem) result, err := testLoad[importTestConfig](filesystemFile(namespace, "config/main.yml"), Options[importTestConfig]{Imports: true, Overlay: "testing"}) if err != nil { t.Fatal(err) } want := map[string]string{"main": "overlaid", "shared": "shared", "alpha": "alpha", "beta": "beta"} if !reflect.DeepEqual(result.Config.Items, want) { t.Fatalf("items = %#v, want %#v", result.Config.Items, want) } sources := result.Sources() wantPaths := []string{"config/main.yml", "shared.yml", "config/fragments/a.yml", "config/fragments/b.yml", "overlays/testing.yml"} gotPaths := make([]string, len(sources)) for index, source := range sources { gotPaths[index] = source.Path } if !reflect.DeepEqual(gotPaths, wantPaths) { t.Fatalf("source paths = %#v, want %#v", gotPaths, wantPaths) } } func TestNativeFilesystemItemImportUsesSharedScopedAssembly(t *testing.T) { t.Parallel() type item struct { ID string `yaml:"id"` Name string `yaml:"name"` } type config struct { Items []item `yaml:"items"` } filesystem := fstest.MapFS{ "main.yml": &fstest.MapFile{Data: []byte("configkit:\n imports:\n - path: records/item.yml\n destination: [items]\n placement: sequence-item\n")}, "records/item.yml": &fstest.MapFile{Data: []byte("configkit:\n imports:\n - path: helper.yml\nid: native\n")}, "records/helper.yml": &fstest.MapFile{Data: []byte("name: helper\n")}, } namespace := newNativeNamespace("bundle", filesystem) result, err := testLoad[config](filesystemFile(namespace, "main.yml"), Options[config]{Imports: true}) if err != nil { t.Fatalf("native item import: %v", err) } if len(result.Config.Items) != 1 || result.Config.Items[0].ID != "native" || result.Config.Items[0].Name != "helper" { t.Fatalf("native item = %#v", result.Config.Items) } sources := result.Sources() if len(sources) != 3 || sources[1].Destination.String() != "/items" || sources[2].Destination.String() != "/items/0" || sources[1].Path != "records/item.yml" || sources[2].Path != "records/helper.yml" { t.Fatalf("native item sources = %#v", sources) } } func TestNativeFilesystemNamespaceAndPathFailures(t *testing.T) { first := fstest.MapFS{"main.yml": &fstest.MapFile{Data: []byte("items: {name: first}\n")}} second := fstest.MapFS{"main.yml": &fstest.MapFile{Data: []byte("items: {name: second}\n")}} firstNamespace := newNativeNamespace("same", first) secondNamespace := newNativeNamespace("same", second) if reflect.TypeOf(first).Comparable() { t.Fatal("test filesystem must have a non-comparable dynamic value") } if firstNamespace.token == secondNamespace.token { t.Fatal("independent filesystem constructors shared identity") } if newNativeAccess(first, ".", firstNamespace.token).identity("main.yml") == newNativeAccess(second, ".", secondNamespace.token).identity("main.yml") { t.Fatal("equal paths from separate filesystems have the same identity") } access := newNativeAccess(first, ".", firstNamespace.token) if access.syntheticIdentity() == access.syntheticIdentity() || access.syntheticIdentity() == access.identity("") { t.Fatal("independent synthetic sources shared a file identity") } for _, test := range []struct { name string namespace *nativeNamespace want string }{ {name: "first", namespace: firstNamespace, want: "first"}, {name: "second", namespace: secondNamespace, want: "second"}, } { t.Run(test.name, func(t *testing.T) { result, err := testLoad[importTestConfig](filesystemFile(test.namespace, "main.yml")) if err != nil || result.Config.Items["name"] != test.want { t.Fatalf("result = %#v, %v", result, err) } }) } for _, name := range []string{".", "../main.yml", "/main.yml", "missing.yml"} { _, diagnostics := readSource(filesystemFile(firstNamespace, name), "source-1", defaultSourceLimits()) if len(diagnostics) != 1 { t.Errorf("%q diagnostics = %#v", name, diagnostics) } } missingImport := fstest.MapFS{"main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: host-only.yml, required: true}]}\n")}} _, err := testLoad[importTestConfig](filesystemFile(newNativeNamespace("bundle", missingImport), "main.yml"), Options[importTestConfig]{Imports: true}) if err == nil { t.Fatal("missing virtual import fell back to the host filesystem") } loadError, ok := err.(*LoadError) if !ok || loadError.Diagnostics()[0].Code != CodeImportMissing { t.Fatalf("missing import error = %#v", err) } } func TestNativeFilesystemClosesFilesOnSuccessAndFailure(t *testing.T) { for _, fallback := range []bool{false, true} { t.Run(map[bool]string{false: "ReadDirFile", true: "ReadDir fallback"}[fallback], func(t *testing.T) { filesystem := &observedFilesystem{files: fstest.MapFS{ "main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{glob: 'parts/*.yml'}]}\nitems: {main: main}\n")}, "parts/one.yml": &fstest.MapFile{Data: []byte("items: {one: one}\n")}, }, fallback: fallback} result, err := testLoad[importTestConfig](filesystemFile(newNativeNamespace("bundle", filesystem), "main.yml"), Options[importTestConfig]{Imports: true}) if err != nil || result.Config.Items["one"] != "one" { t.Fatalf("result = %#v, %v", result, err) } filesystem.assertClosed(t) }) } for _, failure := range []struct { name string file string failRead string failStat string failDir string }{ {name: "read", file: "items: {one: one}\n", failRead: "parts/one.yml"}, {name: "stat", file: "items: {one: one}\n", failStat: "parts/one.yml"}, {name: "parse", file: "items: [unterminated\n"}, {name: "directory", file: "items: {one: one}\n", failDir: "parts"}, } { t.Run(failure.name, func(t *testing.T) { filesystem := &observedFilesystem{files: fstest.MapFS{ "main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{glob: 'parts/*.yml'}]}\n")}, "parts/one.yml": &fstest.MapFile{Data: []byte(failure.file)}, }, failRead: failure.failRead, failStat: failure.failStat, failDir: failure.failDir} _, err := testLoad[importTestConfig](filesystemFile(newNativeNamespace("bundle", filesystem), "main.yml"), Options[importTestConfig]{Imports: true}) if err == nil { t.Fatal("expected a load failure") } if strings.Contains(err.Error(), "tracking failure canary") { t.Fatal("unsafe filesystem error escaped") } filesystem.assertClosed(t) }) } } func TestNativeFilesystemRootAndOptionalImports(t *testing.T) { filesystem := fstest.MapFS{ "config/sub/main.yml": &fstest.MapFile{Data: []byte(`configkit: imports: - path: ../shared.yml - path: absent.yml - glob: absent/*.yml `)}, "config/shared.yml": &fstest.MapFile{Data: []byte("items: {shared: retained}\n")}, "outside.yml": &fstest.MapFile{Data: []byte("items: {outside: blocked}\n")}, } namespace := newNativeNamespace("bundle", filesystem) source := filesystemFile(namespace, "config/sub/main.yml") parsed, diagnostics := parseSource(source, "source-1", defaultParseLimits()) if len(diagnostics) != 0 { t.Fatal(diagnostics) } access := newNativeAccess(filesystem, "config", namespace.token) request := testImportRequest(source, nil, parsed, Path{}, nil, nil) policy := &acquisitionPolicy{importLimits: defaultImportLimits()} state := &acquisitionState{accounting: &importAccounting{}} tier := &tierAcquisitionState{} resolved := resolvedFilesystemContext{native: namespace, rootPath: "config", basePath: "config/sub", mainPath: source.path} composed, sources, diagnostics := acquireImportedWithAccess(request, policy, state, tier, access, resolved) if len(diagnostics) != 0 || composed == nil || len(sources) != 2 || sources[1].Path != "config/shared.yml" { t.Fatalf("contained imports = (%#v, %#v, %#v)", composed, sources, diagnostics) } outside := fstest.MapFS{ "config/sub/main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: ../../outside.yml}]}\n")}, "outside.yml": &fstest.MapFile{Data: []byte("items: {outside: blocked}\n")}, } outsideNamespace := newNativeNamespace("bundle", outside) parsed, diagnostics = parseSource(filesystemFile(outsideNamespace, "config/sub/main.yml"), "source-1", defaultParseLimits()) if len(diagnostics) != 0 { t.Fatal(diagnostics) } request = testImportRequest(filesystemFile(outsideNamespace, "config/sub/main.yml"), nil, parsed, Path{}, nil, nil) policy = &acquisitionPolicy{importLimits: defaultImportLimits()} state = &acquisitionState{accounting: &importAccounting{}} tier = &tierAcquisitionState{} resolved = resolvedFilesystemContext{native: outsideNamespace, rootPath: "config", basePath: "config/sub", mainPath: "config/sub/main.yml"} _, _, diagnostics = acquireImportedWithAccess(request, policy, state, tier, newNativeAccess(outside, "config", outsideNamespace.token), resolved) if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportOutsideRoot { t.Fatalf("escaping import diagnostics = %#v", diagnostics) } } func TestNativeFilesystemDetectsRepeatedAndCyclicImports(t *testing.T) { for _, test := range []struct { name string files fstest.MapFS code Code }{ { name: "repeated", files: fstest.MapFS{ "main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: child.yml}, {path: child.yml}]}\n")}, "child.yml": &fstest.MapFile{Data: []byte("items: {child: child}\n")}, }, code: CodeImportRepeated, }, { name: "cycle", files: fstest.MapFS{ "main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: child.yml}]}\n")}, "child.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: main.yml}]}\n")}, }, code: CodeImportCycle, }, } { t.Run(test.name, func(t *testing.T) { _, err := testLoad[importTestConfig](filesystemFile(newNativeNamespace("bundle", test.files), "main.yml"), Options[importTestConfig]{Imports: true}) loadError, ok := err.(*LoadError) if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != test.code { t.Fatalf("load error = %#v, want %q", err, test.code) } }) } } type observedFilesystem struct { files fstest.MapFS opened int closed int fallback bool failOpen string failRead string failStat string failDir string failReadDir string readDirErr error failNoProgress string failClose string closeErr error failFallbackDir string fallbackErr error fallbackBeforeClose bool handles []*observedFile } func (f *observedFilesystem) Open(name string) (fs.File, error) { if name == f.failOpen { return nil, fs.ErrNotExist } inner, err := f.files.Open(name) if err != nil { return nil, err } f.opened++ file := &observedFile{File: inner, owner: f, name: name} f.handles = append(f.handles, file) if directory, ok := inner.(fs.ReadDirFile); ok && !f.fallback { return &observedDirectoryFile{observedFile: file, directory: directory}, nil } return file, nil } func (f *observedFilesystem) ReadDir(name string) ([]fs.DirEntry, error) { if name == f.failDir { return nil, errors.New("tracking failure canary") } entries, err := fs.ReadDir(f.files, name) if name == f.failFallbackDir { for _, handle := range f.handles { if handle.name == name && !handle.closed { f.fallbackBeforeClose = true } } return entries, f.fallbackErr } return entries, err } func (f *observedFilesystem) assertClosed(t *testing.T) { t.Helper() if f.opened != f.closed || len(f.handles) != f.opened { t.Fatalf("filesystem opens=%d closes=%d handles=%d", f.opened, f.closed, len(f.handles)) } for _, handle := range f.handles { if !handle.closed || handle.closeCalls != 1 { t.Errorf("filesystem handle %q closed=%t close calls=%d", handle.name, handle.closed, handle.closeCalls) } } } type observedFile struct { fs.File owner *observedFilesystem name string closed bool closeCalls int } func (f *observedFile) Read(data []byte) (int, error) { if f.name == f.owner.failRead { return 0, errors.New("tracking failure canary") } return f.File.Read(data) } func (f *observedFile) Stat() (fs.FileInfo, error) { if f.name == f.owner.failStat { return nil, errors.New("tracking failure canary") } return f.File.Stat() } func (f *observedFile) Close() error { f.owner.closed++ f.closeCalls++ f.closed = true err := f.File.Close() if f.name == f.owner.failClose { if f.owner.closeErr != nil { return f.owner.closeErr } return fs.ErrNotExist } return err } type observedDirectoryFile struct { *observedFile directory fs.ReadDirFile } func (f *observedDirectoryFile) ReadDir(count int) ([]fs.DirEntry, error) { if f.name == f.owner.failDir { return nil, errors.New("tracking failure canary") } if f.name == f.owner.failNoProgress { return nil, nil } entries, err := f.directory.ReadDir(count) if f.name == f.owner.failReadDir { if f.owner.readDirErr != nil { return entries, f.owner.readDirErr } return entries, fs.ErrNotExist } return entries, err }