package engine import ( "errors" "os" "path/filepath" "reflect" "runtime" "testing" ) type discoveryLoadConfig struct { Service struct { Settings map[string]string `yaml:"settings"` } `yaml:"service"` Settings map[string]string `yaml:"settings"` Value string `yaml:"value"` } func TestSearchLoadMatchesExplicitFileThroughImportsAndContext(t *testing.T) { t.Parallel() root := t.TempDir() mainPath := writeImportFile(t, root, "app.yml", `configkit: imports: [{path: fragments/extra.yml}] settings: {main: selected} `) importPath := writeImportFile(t, root, "fragments/extra.yml", "settings: {imported: value}\n") search := SearchConfig("app", SearchDirectory(root)) options := Options[discoveryLoadConfig]{ Imports: true, Filesystem: &FilesystemContext{Root: root}, destination: Keys("service"), } searched, err := testLoad(search, options) if err != nil { t.Fatal(err) } explicit, err := testLoad(File(mainPath), options) if err != nil { t.Fatal(err) } assertEquivalentDiscoveryLoads(t, searched.Config, searched.Inspection, explicit.Config, explicit.Inspection, Keys("service", "settings", "main"), Keys("service", "settings", "imported")) sources := searched.Sources() if len(sources) != 2 || sources[0].Kind != SourceKindFile || sources[0].Path != canonicalTestPath(t, mainPath) || sources[1].Path != canonicalTestPath(t, importPath) { t.Fatalf("searched source evidence = %#v", sources) } } func TestSearchLayeredMainAndUnderlayMatchExplicitFiles(t *testing.T) { t.Parallel() root := t.TempDir() mainPath := writeImportFile(t, root, "main.yml", "settings: {main: value}\n") underlayPath := writeImportFile(t, root, "underlay/base.yml", `configkit: imports: [{path: extra.yml}] settings: {underlay: value} `) importPath := writeImportFile(t, root, "underlay/extra.yml", "settings: {imported: value}\n") mainSearch := SearchConfig("", SearchDirectory(root, DirectorySearchOptions{Filenames: []FilenameGroup{CustomFilenames("main.yml")}})) underlaySearch := SearchConfig("", SearchDirectory(filepath.Join(root, "underlay"), DirectorySearchOptions{Filenames: []FilenameGroup{CustomFilenames("base.yml")}})) load := func(main, underlay Source) (*Loaded[discoveryLoadConfig], error) { return LoadLayered( Input{Source: main, Imports: true}, testSecretBinding(), LayeredOptions[discoveryLoadConfig]{Underlays: []Underlay{{Inputs: []Input{{Source: underlay, Imports: true}}}}}, ) } searched, err := load(mainSearch, underlaySearch) if err != nil { t.Fatal(err) } explicit, err := load(File(mainPath), File(underlayPath)) if err != nil { t.Fatal(err) } assertEquivalentDiscoveryLoads(t, searched.Config, searched.Inspection, explicit.Config, explicit.Inspection, Keys("settings", "main"), Keys("settings", "underlay"), Keys("settings", "imported")) sources := searched.Inspection.Sources() if len(sources) != 3 || sources[0].ID != "source-1" || sources[0].Layer != LayerBase || sources[1].ID != "source-2" || sources[1].Layer != LayerUnderlay || sources[2].Path != canonicalTestPath(t, importPath) || sources[2].UnderlayIndex != 1 { t.Fatalf("layered search source evidence = %#v", sources) } } func TestOptionalSearchExhaustionMatchesNoSource(t *testing.T) { t.Parallel() root := t.TempDir() missing := filepath.Join(root, "missing.yml") defaults := func() discoveryLoadConfig { return discoveryLoadConfig{Value: "default"} } options := Options[discoveryLoadConfig]{ Definition: Definition[discoveryLoadConfig]{Defaults: defaults}, Imports: true, Filesystem: &FilesystemContext{Root: root}, } searched, err := testLoad(SearchConfig("", Paths(missing)), options) if err != nil { t.Fatal(err) } explicit, err := testLoad(NoSource(), options) if err != nil { t.Fatal(err) } assertEquivalentDiscoveryLoads(t, searched.Config, searched.Inspection, explicit.Config, explicit.Inspection, Keys("value")) if len(searched.Sources()) != 0 { t.Fatalf("optional exhausted search created sources: %#v", searched.Sources()) } } func TestSearchSelectedFileFailuresDoNotFallBack(t *testing.T) { t.Parallel() t.Run("malformed YAML", func(t *testing.T) { first := writeImportFile(t, t.TempDir(), "first.yml", ": malformed\n") writeImportFile(t, filepath.Dir(first), "later.yml", "value: valid\n") loaded, err := testLoad[discoveryLoadConfig](SearchConfig("", Paths(first, filepath.Join(filepath.Dir(first), "later.yml"))), Options[discoveryLoadConfig]{}) if loaded != nil || err == nil { t.Fatalf("malformed selected source = %#v, %v", loaded, err) } }) t.Run("version check", func(t *testing.T) { root := t.TempDir() first := writeImportFile(t, root, "first.yml", "version: rejected\n") writeImportFile(t, root, "later.yml", "version: accepted\n") var checked []string loaded, err := testLoad[discoveryLoadConfig](SearchConfig("", Paths(first, filepath.Join(root, "later.yml"))), Options[discoveryLoadConfig]{ SourceVersionMode: sourceVersionModeIfPresent, Definition: Definition[discoveryLoadConfig]{Version: &VersionRule{ Path: Keys("version"), Check: func(version Version, _ *Validation) error { checked = append(checked, version.Text) if version.Text == "rejected" { return errors.New("version rejected") } return nil }, }}, }) if loaded != nil || err == nil || !reflect.DeepEqual(checked, []string{"rejected"}) { t.Fatalf("version failure search = %#v, %v; checked %#v", loaded, err, checked) } }) t.Run("decode", func(t *testing.T) { root := t.TempDir() first := writeImportFile(t, root, "first.yml", "value: not-an-integer\n") writeImportFile(t, root, "later.yml", "value: 3\n") type config struct { Value int `yaml:"value"` } loaded, err := testLoad[config](SearchConfig("", Paths(first, filepath.Join(root, "later.yml"))), Options[config]{}) if loaded != nil || err == nil { t.Fatalf("decode failure search = %#v, %v", loaded, err) } }) } func TestSearchValidationPrecedesCandidateProbing(t *testing.T) { t.Parallel() root := t.TempDir() first := writeImportFile(t, root, "first.yml", "value: selected\n") _, err := testLoad[discoveryLoadConfig](SearchConfig("", Searches(Paths(first), Paths(""))), Options[discoveryLoadConfig]{}) var loadErr *LoadError if !errors.As(err, &loadErr) { t.Fatalf("invalid later search error = %v, want *LoadError", err) } if len(loadErr.Diagnostics()) == 0 || loadErr.Diagnostics()[0].Code != CodeOptionsInvalid { t.Fatalf("invalid later search component diagnostics = %#v", loadErr.Diagnostics()) } malformed := writeImportFile(t, t.TempDir(), "main.yml", ": malformed\n") loaded, err := LoadLayered( Input{Source: SearchConfig("", Paths(malformed))}, testSecretBinding(), LayeredOptions[discoveryLoadConfig]{Underlays: []Underlay{{Inputs: []Input{{Source: SearchConfig("", Paths(""))}}}}}, ) if loaded != nil || !errors.As(err, &loadErr) { t.Fatalf("invalid later underlay search = %#v, %v", loaded, err) } for _, diagnostic := range loadErr.Diagnostics() { if diagnostic.Code != CodeOptionsInvalid { t.Fatalf("layered validation acquired main before rejecting underlay: %#v", loadErr.Diagnostics()) } } } func TestSearchContextCompatibilityUsesSelectedOutcome(t *testing.T) { t.Parallel() root := t.TempDir() first := writeImportFile(t, root, "first.yml", "value: first\n") writeImportFile(t, root, "later.yml", "value: later\n") search := SearchConfig("", Paths(first, filepath.Join(root, "later.yml"))) options := Options[discoveryLoadConfig]{Filesystem: &FilesystemContext{BaseDir: root, Root: root}} _, err := testLoad[discoveryLoadConfig](search, options) var loadErr *LoadError if !errors.As(err, &loadErr) { t.Fatalf("selected file with explicit base error = %v, want *LoadError", err) } if len(loadErr.Diagnostics()) == 0 || loadErr.Diagnostics()[0].Code != CodeOptionsInvalid { t.Fatalf("selected file context diagnostics = %#v", loadErr.Diagnostics()) } } func TestSearchSourceObservesFileChangesBetweenLoads(t *testing.T) { t.Parallel() root := t.TempDir() file := writeImportFile(t, root, "config.yml", "value: first\n") source := SearchConfig("", SearchDirectory(root, DirectorySearchOptions{Filenames: []FilenameGroup{CustomFilenames("config.yml")}})) load := func() string { loaded, err := testLoad[discoveryLoadConfig](source, Options[discoveryLoadConfig]{}) if err != nil { t.Fatal(err) } return loaded.Config.Value } if value := load(); value != "first" { t.Fatalf("first search value = %q", value) } if err := os.WriteFile(file, []byte("value: second\n"), 0o600); err != nil { t.Fatal(err) } if value := load(); value != "second" { t.Fatalf("updated search value = %q", value) } } func TestSearchSourceObservesUserConfigEnvironmentChanges(t *testing.T) { if runtime.GOOS == "windows" { t.Setenv("APPDATA", filepath.Join(t.TempDir(), "first")) } else { t.Setenv("XDG_CONFIG_HOME", filepath.Join(t.TempDir(), "first")) } type config struct { Value string `yaml:"value"` } firstBase := os.Getenv("XDG_CONFIG_HOME") if runtime.GOOS == "windows" { firstBase = os.Getenv("APPDATA") } first := writeImportFile(t, firstBase, "app/app.yml", "value: first\n") secondBase := filepath.Join(t.TempDir(), "second") second := writeImportFile(t, secondBase, "app/app.yml", "value: second\n") source := SearchConfig("app", UserConfigDirectory()) load := func() string { loaded, err := testLoad[config](source, Options[config]{}) if err != nil { t.Fatal(err) } return loaded.Config.Value } if value := load(); value != "first" { t.Fatalf("first user-config search value = %q (%s)", value, first) } if runtime.GOOS == "windows" { t.Setenv("APPDATA", secondBase) } else { t.Setenv("XDG_CONFIG_HOME", secondBase) } if value := load(); value != "second" { t.Fatalf("updated user-config search value = %q (%s)", value, second) } // A reached dynamic base failure is terminal, but an earlier match must // prevent the invalid environment value from being resolved at all. direct := writeImportFile(t, t.TempDir(), "direct.yml", "value: direct\n") if runtime.GOOS == "windows" { t.Setenv("APPDATA", "relative") } else { t.Setenv("XDG_CONFIG_HOME", "relative") } early := SearchConfig("app", Searches(Paths(direct), UserConfigDirectory())) loaded, err := testLoad[config](early, Options[config]{}) if err != nil || loaded.Config.Value != "direct" { t.Fatalf("early match with unavailable later base = %#v, %v", loaded, err) } } func TestSearchSourceObservesWorkingDirectoryChanges(t *testing.T) { original, err := os.Getwd() if err != nil { t.Fatal(err) } t.Cleanup(func() { if err := os.Chdir(original); err != nil { t.Errorf("restore working directory: %v", err) } }) firstDir := t.TempDir() secondDir := t.TempDir() writeImportFile(t, firstDir, "app.yml", "value: first\n") writeImportFile(t, secondDir, "app.yml", "value: second\n") source := SearchConfig("app", WorkingDirectory()) load := func() string { loaded, err := testLoad[discoveryLoadConfig](source, Options[discoveryLoadConfig]{}) if err != nil { t.Fatal(err) } return loaded.Config.Value } if err := os.Chdir(firstDir); err != nil { t.Fatal(err) } if value := load(); value != "first" { t.Fatalf("first working-directory search value = %q", value) } if err := os.Chdir(secondDir); err != nil { t.Fatal(err) } if value := load(); value != "second" { t.Fatalf("updated working-directory search value = %q", value) } } func assertEquivalentDiscoveryLoads[T any](t *testing.T, leftConfig T, left *Inspection, rightConfig T, right *Inspection, presencePaths ...Path) { t.Helper() if !reflect.DeepEqual(leftConfig, rightConfig) { t.Fatalf("search config %#v differs from explicit file config %#v", leftConfig, rightConfig) } leftSources, rightSources := left.Sources(), right.Sources() if !reflect.DeepEqual(leftSources, rightSources) { t.Fatalf("search sources %#v differ from explicit file sources %#v", leftSources, rightSources) } leftMetadata, rightMetadata := left.Metadata(), right.Metadata() for _, path := range presencePaths { leftInfo, leftPresent := leftMetadata.Lookup(path) rightInfo, rightPresent := rightMetadata.Lookup(path) if leftPresent != rightPresent || !reflect.DeepEqual(leftInfo, rightInfo) { t.Fatalf("search metadata at %s = %#v, %v; explicit metadata = %#v, %v", path.String(), leftInfo, leftPresent, rightInfo, rightPresent) } } for _, source := range leftSources { leftContext, leftOK := left.SourceContext(source.ID) rightContext, rightOK := right.SourceContext(source.ID) if leftOK != rightOK || leftContext.BaseDir != rightContext.BaseDir || (leftContext.FS != nil) != (rightContext.FS != nil) { t.Fatalf("search context for %s = %#v, %v; explicit context = %#v, %v", source.ID, leftContext, leftOK, rightContext, rightOK) } } }