package engine import ( "errors" "io/fs" "os" "path/filepath" "runtime" "strings" "testing" ) func TestSearchDescriptionEmptyAndSlugRequirements(t *testing.T) { t.Parallel() if diagnostics := validateSearch(Search{}, ""); len(diagnostics) != 0 { t.Fatalf("zero search diagnostics = %#v", diagnostics) } if diagnostics := validateSearch(Searches(), ""); len(diagnostics) != 0 { t.Fatalf("empty concatenation diagnostics = %#v", diagnostics) } if diagnostics := validateSearch(Paths(), ""); len(diagnostics) != 0 { t.Fatalf("empty path list diagnostics = %#v", diagnostics) } if diagnostics := validateSearch(Paths("relative/config.yml"), ""); len(diagnostics) != 0 { t.Fatalf("literal path unexpectedly requires a slug: %#v", diagnostics) } generic := SearchDirectory("configs", DirectorySearchOptions{Filenames: []FilenameGroup{GenericFilenames()}}) if diagnostics := validateSearch(generic, ""); len(diagnostics) != 0 { t.Fatalf("generic-only directory unexpectedly requires a slug: %#v", diagnostics) } if diagnostics := validateSearch(SearchDirectory("configs"), ""); len(diagnostics) == 0 || diagnostics[0].Code != CodeOptionsInvalid { t.Fatalf("default named directory accepted an empty slug: %#v", diagnostics) } if diagnostics := validateSearch(UnixSystemDirectories(), "myapp"); len(diagnostics) != 0 { t.Fatalf("valid preset slug diagnostics = %#v", diagnostics) } } func TestSearchDescriptionSnapshotsInputs(t *testing.T) { t.Parallel() paths := []string{"first.yml", "second.yml"} pathSearch := Paths(paths...) paths[0] = "changed.yml" customNames := []string{"one.yml", "two.yml"} group := CustomFilenames(customNames...) customNames[0] = "changed.yml" groups := []FilenameGroup{group} options := DirectorySearchOptions{Filenames: groups} directorySearch := SearchDirectory("configs", options) groups[0] = GenericFilenames() options.Filenames[0] = NamedFilenames() searches := []Search{pathSearch, directorySearch} combined := Searches(searches...) searches[0] = Search{} if got := combined.entries[0].path; got != "first.yml" { t.Fatalf("snapshotted path = %q", got) } gotNames := combined.entries[2].filenames[0].names if len(gotNames) != 2 || gotNames[0] != "one.yml" || gotNames[1] != "two.yml" { t.Fatalf("snapshotted custom names = %#v", gotNames) } if diagnostics := validateSearch(combined, ""); len(diagnostics) != 0 { t.Fatalf("snapshot description diagnostics = %#v", diagnostics) } } func TestDirectoryFilenameDefaultsAndExplicitEmpty(t *testing.T) { t.Parallel() omitted := SearchDirectory("configs") if len(omitted.entries) != 1 || len(omitted.entries[0].filenames) != 2 || omitted.entries[0].filenames[0].kind != filenameGroupNamed || omitted.entries[0].filenames[1].kind != filenameGroupGeneric { t.Fatalf("omitted groups = %#v", omitted.entries) } explicitNil := SearchDirectory("configs", DirectorySearchOptions{}) if len(explicitNil.entries[0].filenames) != 2 { t.Fatalf("nil filename selection did not use defaults: %#v", explicitNil.entries[0].filenames) } explicitEmpty := SearchDirectory("configs", DirectorySearchOptions{Filenames: []FilenameGroup{}}) if explicitEmpty.entries[0].filenames == nil || len(explicitEmpty.entries[0].filenames) != 0 { t.Fatalf("explicit empty selection lost its distinction: %#v", explicitEmpty.entries[0].filenames) } if len(validateSearch(explicitEmpty, "")) != 0 { t.Fatal("explicit empty filename selection should need no slug") } } func TestSearchDescriptionArityAndErrorConcatenation(t *testing.T) { t.Parallel() tooMany := SearchDirectory("configs", DirectorySearchOptions{}, DirectorySearchOptions{}) if diagnostics := validateSearch(tooMany, "app"); len(diagnostics) == 0 { t.Fatal("multiple option values should be invalid") } invalid := Paths("") combined := Searches(Paths("good.yml"), invalid) if diagnostics := validateSearch(combined, ""); len(diagnostics) == 0 { t.Fatal("concatenation lost a constructor error") } if diagnostics := validateSearch(Paths("good.yml", ""), ""); len(diagnostics) == 0 { t.Fatal("later invalid path was hidden by an earlier component") } } func TestSearchDescriptionPreservesFilenameGroupOrder(t *testing.T) { t.Parallel() groups := []FilenameGroup{GenericFilenames(), NamedFilenames(), CustomFilenames("override.yml")} search := SearchDirectory("configs", DirectorySearchOptions{Filenames: groups}) got := search.entries[0].filenames if len(got) != 3 || got[0].kind != filenameGroupGeneric || got[1].kind != filenameGroupNamed || got[2].names[0] != "override.yml" { t.Fatalf("filename group order = %#v", got) } if got := SearchDirectory("configs", DirectorySearchOptions{Filenames: []FilenameGroup{CustomFilenames()}}).entries[0].filenames[0].names; len(got) != 0 { t.Fatalf("empty custom group = %#v", got) } } func TestFilenameComponentValidation(t *testing.T) { t.Parallel() for _, value := range []string{"", ".", "..", "nested/name", "bad\x00name"} { if validFilenameComponent(value) { t.Errorf("validFilenameComponent(%q) = true", value) } } if runtime.GOOS == "windows" { for _, value := range []string{"CON", "nul.txt", "name.", "bad:name"} { if validFilenameComponent(value) { t.Errorf("Windows component %q unexpectedly accepted", value) } } } else { for _, value := range []string{"name with spaces", "naïve"} { if !validFilenameComponent(value) { t.Errorf("host-valid component %q rejected", value) } } } if diagnostics := validateSearch(SearchDirectory("configs", DirectorySearchOptions{Filenames: []FilenameGroup{CustomFilenames("bad/name")}}), ""); len(diagnostics) == 0 { t.Fatal("invalid custom filename was not retained for validation") } for _, value := range []string{"CON", "nul.txt", "COM¹.log", "LPT³.txt", "name.", "bad:name", "bad\x01name"} { if validWindowsFilenameComponent(value) { t.Errorf("Windows component %q unexpectedly accepted", value) } } for _, value := range []string{"name with spaces", "naïve"} { if !validWindowsFilenameComponent(value) { t.Errorf("valid Windows component %q rejected", value) } } } func TestSearchCandidatesPreserveDirectoryAndGroupOrder(t *testing.T) { t.Parallel() root := t.TempDir() helpers := discoveryDirectoryHelpers{workingDir: func() (string, error) { return root, nil }} search := Searches( Paths("first.yml"), SearchDirectory("settings", DirectorySearchOptions{Filenames: []FilenameGroup{ GenericFilenames(), CustomFilenames("special.yml"), }}), Paths("last.yml"), ) candidates := collectSearchCandidates(t, search, "app", helpers) want := []string{ filepath.Join(root, "first.yml"), filepath.Join(root, "settings", "config.yml"), filepath.Join(root, "settings", "config.yaml"), filepath.Join(root, "settings", "main.yml"), filepath.Join(root, "settings", "main.yaml"), filepath.Join(root, "settings", "special.yml"), filepath.Join(root, "last.yml"), } assertCandidatePaths(t, candidates, want) reversed := SearchDirectory("settings", DirectorySearchOptions{Filenames: []FilenameGroup{NamedFilenames(), GenericFilenames()}}) reversedCandidates := collectSearchCandidates(t, reversed, "app", helpers) assertCandidatePaths(t, reversedCandidates[:2], []string{ filepath.Join(root, "settings", "app.yml"), filepath.Join(root, "settings", "app.yaml"), }) } func TestSearchPresetCandidateLists(t *testing.T) { t.Parallel() helpers := discoveryDirectoryHelpers{ userConfigDir: func() (string, error) { return "/controlled/config", nil }, userHomeDir: func() (string, error) { return "/controlled/home", nil }, workingDir: func() (string, error) { return "/controlled/work", nil }, unixSupported: func() bool { return true }, } unix := collectSearchCandidates(t, UnixSystemDirectories(), "app", helpers) if len(unix) != 14 { t.Fatalf("Unix preset candidate count = %d: %#v", len(unix), unix) } assertCandidatePaths(t, unix, []string{ "/usr/local/etc/app/app.yml", "/usr/local/etc/app/app.yaml", "/usr/local/etc/app/config.yml", "/usr/local/etc/app/config.yaml", "/usr/local/etc/app/main.yml", "/usr/local/etc/app/main.yaml", "/etc/app/app.yml", "/etc/app/app.yaml", "/etc/app/config.yml", "/etc/app/config.yaml", "/etc/app/main.yml", "/etc/app/main.yaml", "/etc/app.yml", "/etc/app.yaml", }) user := collectSearchCandidates(t, UserConfigDirectory(), "app", helpers) assertCandidatePaths(t, user, []string{ "/controlled/config/app/app.yml", "/controlled/config/app/app.yaml", "/controlled/config/app/config.yml", "/controlled/config/app/config.yaml", "/controlled/config/app/main.yml", "/controlled/config/app/main.yaml", "/controlled/config/app.yml", "/controlled/config/app.yaml", }) home := collectSearchCandidates(t, HomeDirectory(), "app", helpers) assertCandidatePaths(t, home, []string{ "/controlled/home/.app/app.yml", "/controlled/home/.app/app.yaml", "/controlled/home/.app/config.yml", "/controlled/home/.app/config.yaml", "/controlled/home/.app/main.yml", "/controlled/home/.app/main.yaml", "/controlled/home/app.yml", "/controlled/home/app.yaml", }) working := collectSearchCandidates(t, WorkingDirectory(), "app", helpers) assertCandidatePaths(t, working, []string{"/controlled/work/app.yml", "/controlled/work/app.yaml"}) } func TestSearchCandidateExpansionIsLazyAndSkipsEmptyDirectories(t *testing.T) { t.Parallel() configCalls := 0 helpers := discoveryDirectoryHelpers{ userConfigDir: func() (string, error) { configCalls++ return "/controlled/config", nil }, workingDir: func() (string, error) { return "/controlled/work", nil }, unixSupported: func() bool { return true }, } search := Searches(Paths("/selected.yml"), UserConfigDirectory()) iterator, diagnostics := newSearchCandidateIterator(search, "app", helpers) if len(diagnostics) != 0 { t.Fatalf("iterator diagnostics = %#v", diagnostics) } if candidate, ok, err := iterator.next(); err != nil || !ok || candidate != "/selected.yml" { t.Fatalf("first candidate = %q, %v, %v", candidate, ok, err) } if configCalls != 0 { t.Fatalf("later user directory resolved before consumption: %d calls", configCalls) } emptyOnly := SearchDirectory("should-not-resolve", DirectorySearchOptions{Filenames: []FilenameGroup{}}) emptyCandidates := collectSearchCandidates(t, emptyOnly, "", helpers) if len(emptyCandidates) != 0 || configCalls != 0 { t.Fatalf("empty directory candidates = %#v, helper calls = %d", emptyCandidates, configCalls) } continued := collectSearchCandidates(t, Searches(emptyOnly, Paths("later.yml")), "", helpers) assertCandidatePaths(t, continued, []string{filepath.Join("/controlled/work", "later.yml")}) if configCalls != 0 { t.Fatalf("empty directory base was resolved: %d calls", configCalls) } } func TestSearchCandidateBaseFailuresAndPlatformApplicability(t *testing.T) { t.Parallel() baseFailure := errors.New("controlled base failure") helpers := discoveryDirectoryHelpers{ userConfigDir: func() (string, error) { return "", baseFailure }, workingDir: func() (string, error) { return "/controlled/work", nil }, unixSupported: func() bool { return false }, } iterator, diagnostics := newSearchCandidateIterator(UserConfigDirectory(), "app", helpers) if len(diagnostics) != 0 { t.Fatalf("user preset validation = %#v", diagnostics) } if _, ok, err := iterator.next(); ok || !errors.Is(err, baseFailure) { t.Fatalf("unavailable user base = ok %v, err %v", ok, err) } iterator, diagnostics = newSearchCandidateIterator(UnixSystemDirectories(), "app", helpers) if len(diagnostics) != 0 { t.Fatalf("Unix preset validation = %#v", diagnostics) } if _, ok, err := iterator.next(); ok || !errors.Is(err, errUnixSystemPresetUnavailable) { t.Fatalf("unsupported Unix preset = ok %v, err %v", ok, err) } iterator, diagnostics = newSearchCandidateIterator(Searches(Paths("/earlier.yml"), UnixSystemDirectories()), "app", helpers) if len(diagnostics) != 0 { t.Fatalf("composed Unix preset validation = %#v", diagnostics) } if candidate, ok, err := iterator.next(); err != nil || !ok || candidate != "/earlier.yml" { t.Fatalf("earlier candidate = %q, %v, %v", candidate, ok, err) } } func TestRelativeAndShellLikeSearchPathsRemainLiteral(t *testing.T) { t.Parallel() root := t.TempDir() helpers := discoveryDirectoryHelpers{workingDir: func() (string, error) { return root, nil }} search := Searches(Paths("~/literal.yml", "$CONFIG/*.yaml"), SearchDirectory("./settings", DirectorySearchOptions{Filenames: []FilenameGroup{CustomFilenames("name with spaces.yml")}})) candidates := collectSearchCandidates(t, search, "", helpers) assertCandidatePaths(t, candidates, []string{ filepath.Join(root, "~/literal.yml"), filepath.Join(root, "$CONFIG/*.yaml"), filepath.Join(root, "settings", "name with spaces.yml"), }) } func collectSearchCandidates(t *testing.T, search Search, application string, helpers discoveryDirectoryHelpers) []string { t.Helper() iterator, diagnostics := newSearchCandidateIterator(search, application, helpers) if len(diagnostics) != 0 { t.Fatalf("search validation = %#v", diagnostics) } var candidates []string for { candidate, ok, err := iterator.next() if err != nil { t.Fatalf("expand candidate: %v", err) } if !ok { return candidates } candidates = append(candidates, candidate) } } func assertCandidatePaths(t *testing.T, got, want []string) { t.Helper() if len(got) != len(want) { t.Fatalf("candidate count = %d, want %d: %#v", len(got), len(want), got) } for i := range want { if got[i] != want[i] { t.Fatalf("candidate %d = %q, want %q", i, got[i], want[i]) } } } func TestResolveSearchSelectsFirstExistingCandidate(t *testing.T) { t.Parallel() root := t.TempDir() first := filepath.Join(root, "first.yml") second := filepath.Join(root, "second.yml") writeDiscoveryFile(t, first, "value: first\n") writeDiscoveryFile(t, second, "value: second\n") selected, diagnostics := resolveSearchSource(Source{}, ptrSearch(Paths(filepath.Join(root, "missing.yml"), first, second)), "", false, discoveryDirectoryHelpers{}, discoveryHostProbe{}) if len(diagnostics) != 0 || selected.kind != sourceFile || selected.path != first { t.Fatalf("selection = %#v, diagnostics %#v", selected, diagnostics) } read, diagnostics := readSource(selected, "source-1", defaultSourceLimits()) if len(diagnostics) != 0 || string(read.data) != "value: first\n" || read.info.Path != canonicalTestPath(t, first) { t.Fatalf("selected source read = %#v, diagnostics %#v", read, diagnostics) } } func TestResolveSearchNamedExtensionsAndNonregularSelection(t *testing.T) { t.Parallel() root := t.TempDir() writeDiscoveryFile(t, filepath.Join(root, "app.yml"), "value: yml\n") writeDiscoveryFile(t, filepath.Join(root, "app.yaml"), "value: yaml\n") selected, diagnostics := resolveSearchSource(Source{}, ptrSearch(SearchDirectory(root)), "app", false, discoveryDirectoryHelpers{}, discoveryHostProbe{}) if len(diagnostics) != 0 || selected.path != filepath.Join(root, "app.yml") { t.Fatalf("named extension selection = %#v, %#v", selected, diagnostics) } directory := filepath.Join(root, "subdirectory") if err := os.Mkdir(directory, 0o700); err != nil { t.Fatal(err) } good := filepath.Join(root, "good.yml") writeDiscoveryFile(t, good, "value: later\n") selected, diagnostics = resolveSearchSource(Source{}, ptrSearch(Paths(directory, good)), "", false, discoveryDirectoryHelpers{}, discoveryHostProbe{}) if len(diagnostics) != 0 || selected.path != directory { t.Fatalf("directory candidate selection = %#v, %#v", selected, diagnostics) } _, diagnostics = readSource(selected, "source-1", defaultSourceLimits()) if len(diagnostics) != 1 || diagnostics[0].Code != CodeSourceNotRegular { t.Fatalf("selected directory read diagnostics = %#v", diagnostics) } } func TestResolveSearchContainedSymlinkAndNoFallbackAfterReadFailure(t *testing.T) { t.Parallel() root := t.TempDir() target := filepath.Join(root, "actual.yml") link := filepath.Join(root, "linked.yml") writeDiscoveryFile(t, target, "value: linked\n") if err := os.Symlink(target, link); err != nil { t.Skipf("symlinks unavailable: %v", err) } writeDiscoveryFile(t, filepath.Join(root, "later.yml"), "value: later\n") selected, diagnostics := resolveSearchSource(Source{}, ptrSearch(Paths(link, filepath.Join(root, "later.yml"))), "", false, discoveryDirectoryHelpers{}, discoveryHostProbe{}) if len(diagnostics) != 0 || selected.path != link { t.Fatalf("symlink selection = %#v, %#v", selected, diagnostics) } read, diagnostics := readSource(selected, "source-1", defaultSourceLimits()) if len(diagnostics) != 0 || string(read.data) != "value: linked\n" || read.info.Path != canonicalTestPath(t, target) { t.Fatalf("symlink read = %#v, %#v", read, diagnostics) } if err := os.Remove(target); err != nil { t.Fatal(err) } _, diagnostics = readSource(selected, "source-1", defaultSourceLimits()) if len(diagnostics) != 1 || diagnostics[0].Code != CodeSourceMissing { t.Fatalf("selected source read failure = %#v", diagnostics) } } func TestResolveSearchRequiredAndOptionalExhaustion(t *testing.T) { t.Parallel() optional, diagnostics := resolveSearchSource(Source{}, ptrSearch(Search{}), "", false, discoveryDirectoryHelpers{}, discoveryHostProbe{}) if len(diagnostics) != 0 || optional.kind != sourceNone { t.Fatalf("optional empty search = %#v, %#v", optional, diagnostics) } _, diagnostics = resolveSearchSource(Source{}, ptrSearch(Search{}), "", true, discoveryDirectoryHelpers{}, discoveryHostProbe{}) if len(diagnostics) != 1 || diagnostics[0].Code != CodeSourceMissing || diagnostics[0].Primary != nil || !strings.Contains(diagnostics[0].Message, "search") { t.Fatalf("required empty search = %#v", diagnostics) } root := t.TempDir() missing := filepath.Join(root, "missing.yml") optional, diagnostics = resolveSearchSource(Source{}, ptrSearch(Paths(missing)), "", false, discoveryDirectoryHelpers{}, discoveryHostProbe{}) if len(diagnostics) != 0 || optional.kind != sourceNone { t.Fatalf("optional missing search = %#v, %#v", optional, diagnostics) } _, diagnostics = resolveSearchSource(Source{}, ptrSearch(Paths(missing)), "", true, discoveryDirectoryHelpers{}, discoveryHostProbe{}) if len(diagnostics) != 1 || diagnostics[0].Code != CodeSourceMissing { t.Fatalf("required missing search = %#v", diagnostics) } } func TestResolveSearchLeavesNonSearchSourcesUntouched(t *testing.T) { t.Parallel() source := Bytes("memory", []byte("value: unchanged\n")) resolved, diagnostics := resolveSearchSource(source, nil, "", false, discoveryDirectoryHelpers{}, discoveryHostProbe{}) if len(diagnostics) != 0 || resolved.kind != sourceBytes || string(resolved.data) != "value: unchanged\n" { t.Fatalf("non-search source = %#v, diagnostics = %#v", resolved, diagnostics) } } func TestResolveSearchStopsOnReachedProbeError(t *testing.T) { t.Parallel() root := t.TempDir() first := filepath.Join(root, "first.yml") second := filepath.Join(root, "second.yml") writeDiscoveryFile(t, first, "value: first\n") writeDiscoveryFile(t, second, "value: second\n") statCalls := 0 probe := discoveryHostProbe{stat: func(path string) (fs.FileInfo, error) { statCalls++ return nil, fs.ErrPermission }} _, diagnostics := resolveSearchSource(Source{}, ptrSearch(Paths(first, second)), "", false, discoveryDirectoryHelpers{}, probe) if len(diagnostics) != 1 || diagnostics[0].Code != CodeSourcePermission || diagnostics[0].Primary != nil || statCalls != 1 { t.Fatalf("probe failure = %#v, stat calls = %d", diagnostics, statCalls) } } func TestResolveSearchLeavesLaterDirectoryUnresolvedAfterSelection(t *testing.T) { t.Parallel() root := t.TempDir() first := filepath.Join(root, "selected.yml") writeDiscoveryFile(t, first, "value: selected\n") baseCalls := 0 helpers := discoveryDirectoryHelpers{userConfigDir: func() (string, error) { baseCalls++ return "", errors.New("later base should not be reached") }} search := ptrSearch(Searches(Paths(first), UserConfigDirectory())) selected, diagnostics := resolveSearchSource(Source{}, search, "app", false, helpers, discoveryHostProbe{}) if len(diagnostics) != 0 || selected.path != first || baseCalls != 0 { t.Fatalf("selected = %#v, diagnostics = %#v, later base calls = %d", selected, diagnostics, baseCalls) } } func TestResolveSearchTreatsShellLikeNamesLiterally(t *testing.T) { t.Parallel() root := t.TempDir() paths := []string{"~/literal.yml", "$CONFIG/literal.yml", "*.yaml"} for _, name := range paths { writeDiscoveryFile(t, filepath.Join(root, name), "value: literal\n") } helper := discoveryDirectoryHelpers{workingDir: func() (string, error) { return root, nil }} for _, name := range paths { selected, diagnostics := resolveSearchSource(Source{}, ptrSearch(Paths(name)), "", false, helper, discoveryHostProbe{}) if len(diagnostics) != 0 || selected.path != filepath.Join(root, name) { t.Fatalf("literal %q selection = %#v, %#v", name, selected, diagnostics) } } } func ptrSearch(search Search) *Search { return &search } func writeDiscoveryFile(t *testing.T, path, contents string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { t.Fatal(err) } }