package configkit import ( "errors" "os" "path/filepath" "reflect" "strings" "testing" "testing/fstest" ) type facadeDiscoveryConfig struct { Value string `yaml:"value"` Settings map[string]string `yaml:"settings"` } type facadeDiscoveryLifecycleConfig struct { Value string `yaml:"value"` Resolved string `yaml:"resolved"` } type facadeDiscoveryPlacementConfig struct { Items []facadeDiscoveryPlacementItem `yaml:"items"` } type facadeDiscoveryPlacementItem struct { Name string `yaml:"name"` } func TestFacadeSearchPresetUsesCurrentWorkingDirectory(t *testing.T) { original, err := os.Getwd() if err != nil { t.Fatal(err) } root := t.TempDir() if err := os.Chdir(root); err != nil { t.Fatal(err) } t.Cleanup(func() { if err := os.Chdir(original); err != nil { t.Errorf("restore working directory: %v", err) } }) writeFacadeImportFile(t, root, "myapp.yml", "value: working\n") result, err := Load[facadeDiscoveryConfig](SearchConfig("myapp", WorkingDirectory())) if err != nil || result.Config.Value != "working" { t.Fatalf("working-directory search = %#v, %v", result, err) } if sources := result.Sources(); len(sources) != 1 || sources[0].Path != canonicalFacadePath(t, filepath.Join(root, "myapp.yml")) { t.Fatalf("working-directory sources = %#v", sources) } } func TestFacadeSearchCustomGenericNamesAndEmptyFilenameSelection(t *testing.T) { t.Parallel() root := t.TempDir() filename := writeFacadeImportFile(t, root, "config.yml", "value: found\n") custom := SearchDirectory(root, DirectorySearchOptions{ Filenames: []FilenameGroup{CustomFilenames("config.yml")}, }) result, err := Load[facadeDiscoveryConfig](SearchConfig("", custom)) if err != nil || result.Config.Value != "found" { t.Fatalf("custom generic-only search with empty slug = %#v, %v", result, err) } if sources := result.Sources(); len(sources) != 1 || sources[0].Path != canonicalFacadePath(t, filename) { t.Fatalf("custom generic-only sources = %#v", sources) } defaults := func() facadeDiscoveryConfig { return facadeDiscoveryConfig{Value: "default"} } empty := SearchDirectory(root, DirectorySearchOptions{Filenames: []FilenameGroup{}}) absent, err := Load(SearchConfig("myapp", empty), Options[facadeDiscoveryConfig]{ Definition: Definition[facadeDiscoveryConfig]{Defaults: defaults}, }) if err != nil || absent.Config.Value != "default" || len(absent.Sources()) != 0 { t.Fatalf("explicit empty filename selection = %#v, %v", absent, err) } omitted, err := Load[facadeDiscoveryConfig](SearchConfig("myapp", SearchDirectory(root))) if err != nil || omitted.Config.Value != "found" { t.Fatalf("omitted filename selection did not use defaults = %#v, %v", omitted, err) } } func TestFacadeSearchOrderingRequirednessAndSnapshots(t *testing.T) { t.Parallel() root := t.TempDir() selectedPath := writeFacadeImportFile(t, root, "config.yml", "value: selected\n") laterPath := writeFacadeImportFile(t, root, "later.yml", "value: later\n") paths := []string{filepath.Join(root, "missing.yml")} names := []string{"config.yml"} groups := []FilenameGroup{CustomFilenames(names...)} directoryOptions := []DirectorySearchOptions{{Filenames: groups}} search := Searches(Paths(paths...), SearchDirectory(root, directoryOptions...), Paths(laterPath)) source := SearchConfig("", search, SearchOptions{Required: true}) paths[0] = laterPath names[0] = "later.yml" groups[0] = CustomFilenames("later.yml") directoryOptions[0].Filenames[0] = CustomFilenames("later.yml") search = Paths(laterPath) result, err := Load[facadeDiscoveryConfig](source) if err != nil || result.Config.Value != "selected" { t.Fatalf("ordered required search after caller mutation = %#v, %v", result, err) } if sources := result.Sources(); len(sources) != 1 || sources[0].Path != canonicalFacadePath(t, selectedPath) { t.Fatalf("ordered search sources = %#v", sources) } _, err = Load[facadeDiscoveryConfig](SearchConfig("", Paths(filepath.Join(root, "missing.yml")), SearchOptions{Required: true})) var loadErr *LoadError if !errors.As(err, &loadErr) { t.Fatalf("required exhausted search error = %v, want *LoadError", err) } foundMissing := false for _, diagnostic := range loadErr.Diagnostics() { foundMissing = foundMissing || diagnostic.Code == CodeSourceMissing } if !foundMissing { t.Fatalf("required exhausted search diagnostics = %#v", loadErr.Diagnostics()) } } func TestFacadeSearchWorksForLayeredMainAndUnderlay(t *testing.T) { t.Parallel() root := t.TempDir() mainPath := writeFacadeImportFile(t, root, "main.yml", "settings: {main: value}\n") underlayPath := writeFacadeImportFile(t, root, "underlay/base.yml", "settings: {underlay: value}\n") result, err := LoadLayered( Input{Source: SearchConfig("", Paths(mainPath))}, LayeredOptions[facadeDiscoveryConfig]{Underlays: []Underlay{{Inputs: []Input{{ Source: SearchConfig("", SearchDirectory(filepath.Dir(underlayPath), DirectorySearchOptions{ Filenames: []FilenameGroup{CustomFilenames("base.yml")}, })), }}}}}, ) if err != nil || !reflect.DeepEqual(result.Config.Settings, map[string]string{"main": "value", "underlay": "value"}) { t.Fatalf("layered search = %#v, %v", result, err) } sources := result.Sources() if len(sources) != 2 || sources[0].Path != canonicalFacadePath(t, mainPath) || sources[0].Layer != LayerBase || sources[1].Path != canonicalFacadePath(t, underlayPath) || sources[1].Layer != LayerUnderlay { t.Fatalf("layered search sources = %#v", sources) } } func TestFacadeSearchSelectedAndAbsentUseRootOnlyContext(t *testing.T) { t.Parallel() root := t.TempDir() mainPath := writeFacadeImportFile(t, root, "main.yml", "value: main\n") writeFacadeImportFile(t, root, "overlays/testing.yml", "value: overlay\n") filesystem := &FilesystemContext{Root: root} options := Options[facadeDiscoveryConfig]{Overlay: "testing", Filesystem: filesystem} selected, err := Load(SearchConfig("", Paths(mainPath)), options) if err != nil || selected.Config.Value != "overlay" { t.Fatalf("selected search with root-only context = %#v, %v", selected, err) } sources := selected.Sources() if len(sources) != 2 || sources[0].Path != canonicalFacadePath(t, mainPath) || sources[0].ID != "source-1" || sources[1].ID != "source-2" || sources[1].Layer != LayerOverlay { t.Fatalf("selected search source evidence = %#v", sources) } mainContext, ok := selected.SourceContext("source-1") if !ok || mainContext.BaseDir != "." { t.Fatalf("selected file base context = %#v, %v", mainContext, ok) } missing := SearchConfig("", Paths(filepath.Join(root, "missing.yml"))) searchedAbsent, err := Load(missing, options) if err != nil { t.Fatalf("exhausted search with root-only overlay context: %v", err) } explicitAbsent, err := Load(NoSource(), options) if err != nil { t.Fatalf("NoSource with root-only overlay context: %v", err) } if !reflect.DeepEqual(searchedAbsent.Config, explicitAbsent.Config) || !reflect.DeepEqual(searchedAbsent.Sources(), explicitAbsent.Sources()) { t.Fatalf("searched absence differs from NoSource: %#v / %#v", searchedAbsent, explicitAbsent) } left, leftOK := searchedAbsent.Metadata().Lookup(Keys("value")) right, rightOK := explicitAbsent.Metadata().Lookup(Keys("value")) if leftOK != rightOK || !reflect.DeepEqual(left, right) || !leftOK { t.Fatalf("searched absence metadata = %#v, %v; NoSource metadata = %#v, %v", left, leftOK, right, rightOK) } if sources := searchedAbsent.Sources(); len(sources) != 1 || sources[0].ID != "source-1" || sources[0].Layer != LayerOverlay { t.Fatalf("absent search created unexpected source evidence = %#v", sources) } } func TestFacadeSearchRejectsSelectedContextWithoutFallback(t *testing.T) { t.Parallel() root := t.TempDir() inside := writeFacadeImportFile(t, root, "inside.yml", "value: fallback\n") outsideRoot := t.TempDir() outside := writeFacadeImportFile(t, outsideRoot, "outside.yml", "value: selected\n") search := SearchConfig("", Paths(outside, inside)) _, err := Load(search, Options[facadeDiscoveryConfig]{Filesystem: &FilesystemContext{Root: root}}) var loadErr *LoadError if !errors.As(err, &loadErr) || !hasFacadeDiagnostic(loadErr, CodeImportOutsideRoot) { t.Fatalf("selected outside-root file error = %v, want containment failure", err) } _, err = Load(search, Options[facadeDiscoveryConfig]{Filesystem: &FilesystemContext{BaseDir: root, Root: root}}) if !errors.As(err, &loadErr) || !hasFacadeDiagnostic(loadErr, CodeOptionsInvalid) { t.Fatalf("selected file with explicit base error = %v, want option failure", err) } native := NewFilesystem("native", fstest.MapFS{}) _, err = Load(search, Options[facadeDiscoveryConfig]{Filesystem: &FilesystemContext{FS: &native, BaseDir: ".", Root: "."}}) if !errors.As(err, &loadErr) || !hasFacadeDiagnostic(loadErr, CodeOptionsInvalid) { t.Fatalf("selected file with native context error = %v, want option failure", err) } } func TestFacadeSearchRunsCallbacksOnceAndKeepsInspectionWithSelectedFile(t *testing.T) { t.Parallel() root := t.TempDir() selectedPath := writeFacadeImportFile(t, root, "selected.yml", "value: selected\n") writeFacadeImportFile(t, root, "later.yml", "value: later\n") defaultsCalls, resolveCalls, validateCalls := 0, 0, 0 result, err := Load(SearchConfig("", Paths(selectedPath, filepath.Join(root, "later.yml"))), Options[facadeDiscoveryLifecycleConfig]{ Definition: Definition[facadeDiscoveryLifecycleConfig]{ Defaults: func() facadeDiscoveryLifecycleConfig { defaultsCalls++ return facadeDiscoveryLifecycleConfig{} }, Resolve: func(configuration *facadeDiscoveryLifecycleConfig, resolution *Resolution) error { resolveCalls++ configuration.Resolved = "from:" + configuration.Value resolution.Record(Keys("resolved"), ChangeNormalization, "derive from selected value", Keys("value")) return nil }, Validate: func(_ *facadeDiscoveryLifecycleConfig, _ *Validation) { validateCalls++ }, }, }) if err != nil || result.Config.Value != "selected" || result.Config.Resolved != "from:selected" { t.Fatalf("search lifecycle result = %#v, %v", result, err) } if defaultsCalls != 1 || resolveCalls != 1 || validateCalls != 1 { t.Fatalf("search callbacks ran defaults=%d resolve=%d validate=%d times", defaultsCalls, resolveCalls, validateCalls) } sources := result.Sources() if len(sources) != 1 || sources[0].Path != canonicalFacadePath(t, selectedPath) || sources[0].ID != "source-1" { t.Fatalf("selected source evidence = %#v", sources) } valueInfo, found := result.Metadata().Lookup(Keys("value")) if !found || len(valueInfo.Origin.Locations) != 1 || valueInfo.Origin.Locations[0].SourceID != sources[0].ID { t.Fatalf("selected value metadata = %#v, %v", valueInfo, found) } var rendered strings.Builder if err := result.WriteEffective(&rendered); err != nil || !strings.Contains(rendered.String(), "value: selected") || strings.Contains(rendered.String(), "later") { t.Fatalf("effective output = %q, %v", rendered.String(), err) } } func TestFacadeSearchSequenceItemAndAbsentInputPlacement(t *testing.T) { t.Parallel() root := t.TempDir() selectedPath := writeFacadeImportFile(t, root, "item.yml", "name: selected\n") options := Options[facadeDiscoveryPlacementConfig]{ Destination: Keys("items"), PlacementMode: PlacementModeSequenceItem, } selected, err := Load(SearchConfig("", Paths(selectedPath)), options) if err != nil || len(selected.Config.Items) != 1 || selected.Config.Items[0].Name != "selected" { t.Fatalf("selected sequence item = %#v, %v", selected, err) } if sources := selected.Sources(); len(sources) != 1 || sources[0].Destination.String() != "/items" || sources[0].PlacementMode != PlacementModeSequenceItem { t.Fatalf("selected sequence-item source = %#v", sources) } defaults := func() facadeDiscoveryPlacementConfig { return facadeDiscoveryPlacementConfig{Items: []facadeDiscoveryPlacementItem{}} } absent, err := Load(SearchConfig("", Paths(filepath.Join(root, "missing.yml"))), Options[facadeDiscoveryPlacementConfig]{ Definition: Definition[facadeDiscoveryPlacementConfig]{Defaults: defaults}, Destination: options.Destination, PlacementMode: options.PlacementMode, }) if err != nil || len(absent.Config.Items) != 0 || len(absent.Sources()) != 0 { t.Fatalf("absent search created a sequence item: %#v, %v", absent, err) } } func hasFacadeDiagnostic(err *LoadError, code Code) bool { for _, diagnostic := range err.Diagnostics() { if diagnostic.Code == code { return true } } return false } func canonicalFacadePath(t *testing.T, path string) string { t.Helper() absolute, err := filepath.Abs(path) if err != nil { t.Fatal(err) } canonical, err := filepath.EvalSymlinks(absolute) if err != nil { t.Fatal(err) } return canonical }