package engine import ( "errors" "os" "path/filepath" "reflect" "runtime" "strconv" "strings" "testing" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) type importTestConfig struct { Settings struct { Root string `yaml:"root"` First string `yaml:"first"` Nested string `yaml:"nested"` Second string `yaml:"second"` } `yaml:"settings"` Version string `yaml:"version"` Items map[string]string `yaml:"items"` } func TestLoadFormerDirectiveKeyIsApplicationData(t *testing.T) { type config struct { Legacy struct { Imports []struct { Path string `yaml:"path"` } `yaml:"imports"` } `yaml:"_configkit"` } for _, imports := range []bool{false, true} { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "_configkit:\n imports:\n - path: nonexistent.yml\n") result, err := testLoad[config](File(mainPath), Options[config]{Imports: imports}) if err != nil { t.Fatalf("Imports=%v: %v", imports, err) } if len(result.Config.Legacy.Imports) != 1 || result.Config.Legacy.Imports[0].Path != "nonexistent.yml" || len(result.Sources()) != 1 { t.Fatalf("Imports=%v: former directive was not retained as application data: %#v", imports, result) } } } func TestLoadTraversesPathImportsDepthFirstAndComposesOrigins(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - path: first.yml - path: second.yml settings: root: root `) firstPath := writeImportFile(t, directory, "first.yml", `configkit: imports: - path: nested/third.yml settings: first: first `) thirdPath := writeImportFile(t, directory, "nested/third.yml", "settings:\n nested: nested\n") secondPath := writeImportFile(t, directory, "second.yml", "settings:\n second: second\n") result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true}) if err != nil { t.Fatal(err) } if got := result.Config.Settings; got.Root != "root" || got.First != "first" || got.Nested != "nested" || got.Second != "second" { t.Fatalf("composed settings = %#v", got) } sources := result.Sources() wantPaths := []string{canonicalTestPath(t, mainPath), canonicalTestPath(t, firstPath), canonicalTestPath(t, thirdPath), canonicalTestPath(t, secondPath)} gotPaths := make([]string, len(sources)) for index, source := range sources { gotPaths[index] = source.Path if source.ID != "source-"+strconv.Itoa(index+1) { t.Errorf("source %d ID = %q", index, source.ID) } if index > 0 && source.ImportedFrom == nil { t.Errorf("source %d has no import location", index) } } if !reflect.DeepEqual(gotPaths, wantPaths) { t.Fatalf("source paths = %#v, want depth-first %#v", gotPaths, wantPaths) } info, ok := result.Metadata().Lookup(Keys("settings")) if !ok || !info.Explicit || len(info.Origin.Locations) != 4 { t.Fatalf("settings metadata = %#v, %v", info, ok) } } func TestLoadOrdinaryImportsUseDependencyReadyCompositionOrder(t *testing.T) { type task struct { ID string `yaml:"id"` } type config struct { Tasks []task `yaml:"tasks"` } directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - path: z-parent.yml - path: a-independent.yml tasks: [{id: main}] `) zParentPath := writeImportFile(t, directory, "z-parent.yml", `configkit: imports: - path: b-dependency.yml tasks: [{id: z-parent}] `) aIndependentPath := writeImportFile(t, directory, "a-independent.yml", "tasks: [{id: a-independent}]\n") bDependencyPath := writeImportFile(t, directory, "b-dependency.yml", "tasks: [{id: b-dependency}]\n") input, diagnostics := testAcquireInput(inputAcquisitionRequest{ input: loadInput{source: File(mainPath), imports: true}, role: sourceVersionMain, directiveContext: rootDirectiveContextImportsOnly, }, acquisitionPolicy{ sequencePolicy: mustCompileSequenceIdentityPolicy(t, []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("tasks")}, identityFields: []string{"id"}}}), importLimits: defaultImportLimits(), }, nil) if len(diagnostics) != 0 { t.Fatalf("assemble imports: %#v", diagnostics) } tasks, ok := documentAtPath(input.document, Keys("tasks")) if !ok || tasks.Kind != document.Sequence { t.Fatalf("tasks document = %#v, %v", tasks, ok) } wantOrder := []string{"a-independent", "b-dependency", "z-parent", "main"} if len(tasks.Sequence) != len(wantOrder) { t.Fatalf("tasks = %#v", tasks.Sequence) } for index, want := range wantOrder { id, found := documentAtPath(tasks, Path{}.Index(index).Key("id")) if !found || id.Text != want { t.Errorf("task %d ID = %#v, want %q", index, id, want) } } wantAcquisition := []string{ canonicalTestPath(t, mainPath), canonicalTestPath(t, zParentPath), canonicalTestPath(t, bDependencyPath), canonicalTestPath(t, aIndependentPath), } for index, want := range wantAcquisition { if got := input.sources[index].Path; got != want { t.Errorf("acquired source %d = %q, want %q", index, got, want) } } } func TestAcquiredContextsUseSelectedFileWhenSourceRecordsAreReordered(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "settings: {root: main}\n") importedPath := writeImportFile(t, directory, "nested/imported.yml", "settings: {nested: imported}\n") sources := []SourceInfo{ {ID: "source-2", Path: canonicalTestPath(t, importedPath), Kind: SourceKindFile}, {ID: "source-1", Path: canonicalTestPath(t, mainPath), Kind: SourceKindFile}, } mainInfo := &SourceInfo{Path: canonicalTestPath(t, mainPath)} resolved, diagnostic := resolveFilesystemContext(File(mainPath), nil, parsedSource{source: mainInfo}, nil) if diagnostic != nil { t.Fatalf("resolve main context: %#v", diagnostic) } contexts, diagnostic := contextsForResolvedSources(&resolved, sources) if diagnostic != nil { t.Fatalf("source contexts: %#v", diagnostic) } if got := contexts["source-1"].BaseDir; got != "." { t.Errorf("main source base = %q, want root-relative current directory", got) } if got := contexts["source-2"].BaseDir; got != "nested" { t.Errorf("imported source base = %q, want nested", got) } } func TestLoadImportsFromProfilesDirectoryAsBaseInput(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: profiles/shared.yml}]}\n") writeImportFile(t, directory, "profiles/shared.yml", "settings: {root: imported}\n") result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true}) if err != nil || result.Config.Settings.Root != "imported" || len(result.Sources()) != 2 || result.Sources()[1].Layer != LayerBase { t.Fatalf("profiles import = %#v, %v", result, err) } } func TestLoadRejectsImportConflictsAndFragmentVersions(t *testing.T) { t.Run("all duplicate claimants", func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - path: one.yml - path: two.yml - path: three.yml `) for index, name := range []string{"one.yml", "two.yml", "three.yml"} { writeImportFile(t, directory, name, "settings:\n root: "+string(rune('a'+index))+"\n") } loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeComposeConflict) diagnostic := loadError.Diagnostics()[0] if diagnostic.Path.String() != "/settings/root" || diagnostic.Primary == nil || len(diagnostic.Related) != 2 { t.Fatalf("conflict diagnostic = %#v", diagnostic) } }) versionRule := &VersionRule{Path: Keys("settings", "version"), Check: func(Version, *Validation) error { return nil }} for _, test := range []struct { name string fragment string }{ {name: "exact integer", fragment: "settings:\n version: 4\n"}, {name: "ancestor scalar", fragment: "settings: blocked\n"}, } { t.Run(test.name, func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports:\n - path: fragment.yml\n") writeImportFile(t, directory, "fragment.yml", test.fragment) loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true, Definition: Definition[importTestConfig]{Version: versionRule}}, CodeImportVersion) diagnostic := loadError.Diagnostics()[0] if diagnostic.Path.String() != "/settings/version" || diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-2" { t.Fatalf("version placement diagnostic = %#v", diagnostic) } }) } } func TestLoadImportDirectiveValidationAndTemporaryGlobGate(t *testing.T) { tests := []struct { name string body string code Code }{ {name: "directive kind", body: "configkit: []\n", code: CodeImportInvalid}, {name: "unknown directive field", body: "configkit: {unknown: true}\n", code: CodeImportInvalid}, {name: "imports kind", body: "configkit: {imports: {}}\n", code: CodeImportInvalid}, {name: "declaration kind", body: "configkit: {imports: [file.yml]}\n", code: CodeImportInvalid}, {name: "missing target", body: "configkit: {imports: [{required: true}]}\n", code: CodeImportInvalid}, {name: "two targets", body: "configkit: {imports: [{path: a.yml, glob: '*.yml'}]}\n", code: CodeImportInvalid}, {name: "unknown declaration field", body: "configkit: {imports: [{path: a.yml, extra: true}]}\n", code: CodeImportInvalid}, {name: "removed optional field", body: "configkit: {imports: [{path: a.yml, optional: true}]}\n", code: CodeImportInvalid}, {name: "required kind", body: "configkit: {imports: [{path: a.yml, required: 'yes'}]}\n", code: CodeImportInvalid}, {name: "placement kind", body: "configkit: {imports: [{path: a.yml, placement: true}]}\n", code: CodeImportInvalid}, {name: "placement value", body: "configkit: {imports: [{path: a.yml, placement: item}]}\n", code: CodeImportInvalid}, {name: "nested directive", body: "nested: {configkit: {}}\n", code: CodeImportInvalid}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", test.body) requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, test.code) }) } } func TestLoadItemImportComposesHelpersAndNestedItemsInDeclaringScope(t *testing.T) { t.Parallel() type nested struct { Value string `yaml:"value"` } type record struct { ID string `yaml:"id"` Name string `yaml:"name"` Label string `yaml:"label"` Metadata map[string]string `yaml:"metadata"` Children []nested `yaml:"children"` } type config struct { Items []record `yaml:"items"` } directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - path: records/item.yml destination: [items] placement: sequence-item `) itemPath := writeImportFile(t, directory, "records/item.yml", `configkit: imports: - path: nested/helper.yml - path: child.yml destination: [children] placement: sequence-item name: authored `) helperPath := writeImportFile(t, directory, "records/nested/helper.yml", `configkit: imports: - path: more.yml destination: [metadata] id: supplied label: helper `) morePath := writeImportFile(t, directory, "records/nested/more.yml", "flavor: transitive\n") childPath := writeImportFile(t, directory, "records/child.yml", "value: nested\n") result, err := testLoad[config](File(mainPath), Options[config]{Imports: true}) if err != nil { t.Fatalf("load item import: %v", err) } if len(result.Config.Items) != 1 { t.Fatalf("items = %#v", result.Config.Items) } item := result.Config.Items[0] if item.ID != "supplied" || item.Name != "authored" || item.Label != "helper" || item.Metadata["flavor"] != "transitive" || len(item.Children) != 1 || item.Children[0].Value != "nested" { t.Fatalf("composed item = %#v", item) } if got := result.Sources(); len(got) != 5 || got[1].Path != canonicalTestPath(t, itemPath) || got[2].Path != canonicalTestPath(t, helperPath) || got[3].Path != canonicalTestPath(t, morePath) || got[4].Path != canonicalTestPath(t, childPath) { t.Fatalf("source traversal = %#v", got) } for _, test := range []struct { index int want string }{ {index: 1, want: "/items"}, {index: 2, want: "/items/0"}, {index: 3, want: "/items/0/metadata"}, {index: 4, want: "/items/0/children"}, } { if got := result.Sources()[test.index].Destination.String(); got != test.want { t.Errorf("source %d destination = %q, want %q", test.index+1, got, test.want) } } for _, test := range []struct { path Path sourceID string }{ {path: Keys("items").Index(0).Key("id"), sourceID: "source-3"}, {path: Keys("items").Index(0).Key("metadata").Key("flavor"), sourceID: "source-4"}, } { info, ok := result.Metadata().Lookup(test.path) if !ok || !info.Explicit || len(info.Origin.Locations) != 1 || info.Origin.Locations[0].SourceID != test.sourceID { t.Errorf("origin at %s = %#v, %v, want source %s", test.path.String(), info, ok, test.sourceID) } foundInHistory := false for _, change := range result.Metadata().History(test.path) { for _, location := range change.Origin.Locations { foundInHistory = foundInHistory || location.SourceID == test.sourceID } } if !foundInHistory { t.Errorf("history at %s does not retain source %s", test.path.String(), test.sourceID) } } t.Run("duplicate helper identity remains a mapping conflict", func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - path: item.yml destination: [items] placement: sequence-item `) writeImportFile(t, directory, "item.yml", `configkit: imports: - path: helper.yml id: authored `) writeImportFile(t, directory, "helper.yml", "id: supplied\n") _, err := testLoad[config](File(mainPath), Options[config]{Imports: true}) var loadError *LoadError if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 { t.Fatalf("duplicate identity error = %v", err) } diagnostic := loadError.Diagnostics()[0] if diagnostic.Code != CodeComposeConflict || diagnostic.Path.String() != "/items/0/id" || diagnostic.Primary == nil || len(diagnostic.Related) != 1 { t.Fatalf("duplicate identity diagnostic = %#v", diagnostic) } }) } func TestLayeredUnderlayItemImportRetainsNestedEvidenceAndDestinations(t *testing.T) { t.Parallel() type nested struct { Value string `yaml:"value"` } type record struct { ID string `yaml:"id"` Name string `yaml:"name"` Label string `yaml:"label"` Metadata map[string]string `yaml:"metadata"` Children []nested `yaml:"children"` } type config struct { Catalog struct { Entries []record `yaml:"entries"` } `yaml:"catalog"` } directory := t.TempDir() itemPath := writeImportFile(t, directory, "records/item.yml", `configkit: imports: - path: nested/helper.yml - path: children/child.yml destination: [children] placement: sequence-item name: authored `) helperPath := writeImportFile(t, directory, "records/nested/helper.yml", `configkit: imports: - path: more.yml destination: [metadata] id: supplied label: helper `) morePath := writeImportFile(t, directory, "records/nested/more.yml", "flavor: transitive\n") childPath := writeImportFile(t, directory, "records/children/child.yml", "value: nested\n") loaded, err := LoadLayered( Input{Source: NoSource(), Imports: true}, testSecretBinding(), LayeredOptions[config]{Underlays: []Underlay{{Inputs: []Input{{ Source: File(itemPath), Imports: true, Destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem, }}}}}, ) if err != nil { if loadError, ok := err.(*LoadError); ok { t.Fatalf("LoadLayered: %v, diagnostics %#v", err, loadError.Diagnostics()) } t.Fatalf("LoadLayered: %v", err) } result := testLoaded(loaded) if len(result.Config.Catalog.Entries) != 1 || result.Config.Catalog.Entries[0].ID != "supplied" || result.Config.Catalog.Entries[0].Name != "authored" || result.Config.Catalog.Entries[0].Label != "helper" || result.Config.Catalog.Entries[0].Metadata["flavor"] != "transitive" || len(result.Config.Catalog.Entries[0].Children) != 1 || result.Config.Catalog.Entries[0].Children[0].Value != "nested" { t.Fatalf("underlay item = %#v", result.Config.Catalog.Entries) } sources := result.Sources() wantSources := []struct { path string destination string }{ {path: itemPath, destination: "/catalog/entries"}, {path: helperPath, destination: "/catalog/entries/0"}, {path: morePath, destination: "/catalog/entries/0/metadata"}, {path: childPath, destination: "/catalog/entries/0/children"}, } if len(sources) != len(wantSources) { t.Fatalf("sources = %#v", sources) } for index, want := range wantSources { if sources[index].Path != canonicalTestPath(t, want.path) || sources[index].Destination.String() != want.destination { t.Errorf("source %d = %#v, want path %q and destination %q", index+1, sources[index], want.path, want.destination) } } for _, test := range []struct { path Path sourceID string }{ {path: Keys("catalog", "entries").Index(0).Key("id"), sourceID: "source-2"}, {path: Keys("catalog", "entries").Index(0).Key("metadata").Key("flavor"), sourceID: "source-3"}, {path: Keys("catalog", "entries").Index(0).Key("children").Index(0).Key("value"), sourceID: "source-4"}, } { info, ok := result.Metadata().Lookup(test.path) if !ok || !info.Explicit || len(info.Origin.Locations) != 1 || info.Origin.Locations[0].SourceID != test.sourceID || info.Origin.Layer != LayerUnderlay { t.Errorf("origin at %s = %#v, %v, want %s in underlay", test.path.String(), info, ok, test.sourceID) } foundInHistory := false for _, change := range result.Metadata().History(test.path) { for _, location := range change.Origin.Locations { foundInHistory = foundInHistory || location.SourceID == test.sourceID } } if !foundInHistory { t.Errorf("history at %s does not retain source %s", test.path.String(), test.sourceID) } } } func TestLoadItemGlobKeepsDefaultModeIndependentAndOptionalMissesAbsent(t *testing.T) { t.Parallel() type item struct { Value string `yaml:"value"` } type config struct { Items []item `yaml:"items"` Other map[string]string `yaml:"other"` } directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - glob: records/*.yml destination: [items] placement: sequence-item - glob: absent/*.yml destination: [items] placement: sequence-item - path: other.yml destination: [other] `) emptyPath := writeImportFile(t, directory, "records/empty.yml", "{}\n") writeImportFile(t, directory, "other.yml", "value: mapping-default\n") result, err := testLoad[config](File(mainPath), Options[config]{Imports: true}) if err != nil { t.Fatalf("load item glob: %v", err) } if len(result.Config.Items) != 1 || result.Config.Items[0].Value != "" || result.Config.Other["value"] != "mapping-default" { t.Fatalf("config = %#v", result.Config) } sources := result.Sources() if len(sources) != 3 || sources[1].Path != canonicalTestPath(t, emptyPath) || sources[1].Destination.String() != "/items" || sources[1].placementMode != placementModeSequenceItem || sources[2].placementMode != placementModeMapping || sources[2].Destination.String() != "/other" { t.Fatalf("source placement evidence = %#v", sources) } } func TestLoadMainItemPlacementKeepsItsHelpersInTheItem(t *testing.T) { t.Parallel() type item struct { ID string `yaml:"id"` Value string `yaml:"value"` } type config struct { Items []item `yaml:"items"` } directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports:\n - path: helper.yml\nid: main\n") helperPath := writeImportFile(t, directory, "helper.yml", "value: helper\n") result, err := testLoad[config](File(mainPath), Options[config]{ Imports: true, destination: Keys("items"), placementMode: placementModeSequenceItem, }) if err != nil { t.Fatalf("load main item: %v", err) } if len(result.Config.Items) != 1 || result.Config.Items[0].ID != "main" || result.Config.Items[0].Value != "helper" { t.Fatalf("main item = %#v", result.Config.Items) } sources := result.Sources() if len(sources) != 2 || sources[0].Destination.String() != "/items" || sources[0].placementMode != placementModeSequenceItem || sources[1].Destination.String() != "/items/0" || sources[1].placementMode != placementModeMapping { t.Fatalf("main item sources = %#v", sources) } if sources[1].Path != canonicalTestPath(t, helperPath) { t.Fatalf("helper path = %q", sources[1].Path) } } func TestProjectAcquiredDestinationsUsesMappedSequenceItemIndex(t *testing.T) { child := &composedDocument{} childScope := acquisitionScope{contributions: []acquiredContribution{{ source: SourceInfo{ID: "nested-source"}, destination: Keys("settings"), }}} child.acquisition = []acquisitionScope{childScope} composed := &composedDocument{sequenceRanges: []sequenceRange{{ sourceIndex: 0, path: Keys("records"), start: 0, end: 1, items: []sequenceItemProjection{{inputIndex: 0, outputIndex: 2}}, }}} scope := acquisitionScope{contributions: []acquiredContribution{{ source: SourceInfo{ID: "outer-source", placementMode: placementModeSequenceItem}, destination: Keys("records"), nested: child, }}} sources := []SourceInfo{{ID: "outer-source"}, {ID: "nested-source"}} projectAcquiredDestinations(composed, scope, sources) if got, want := sources[0].Destination.String(), "/records"; got != want { t.Errorf("outer source destination = %q, want %q", got, want) } if got, want := sources[1].Destination.String(), "/records/2/settings"; got != want { t.Errorf("nested source destination = %q, want %q", got, want) } } func TestProjectNestedEvidenceUsesMappedSequenceItemIndex(t *testing.T) { outer := historyApplication{ value: parseCompositionDocument(t, "outer-source", "records: [{id: outer}]\n"), sequences: []sequenceHistoryContribution{{ path: Keys("records"), mode: sequenceHistoryMapped, items: []sequenceItemProjection{{inputIndex: 0, outputIndex: 2}}, }}, } outer.sequencesByPath = indexSequenceHistoryContributions(outer.sequences) nestedRoot := parseCompositionDocument(t, "nested-source", "settings: {value: nested}\n") nestedOrigin := Origin{Layer: LayerBase, Locations: []Location{{SourceID: "nested-source"}}} nested := &composedDocument{ root: nestedRoot, applications: []historyApplication{{value: nestedRoot}}, origins: map[string]Origin{"/settings/value": nestedOrigin}, } scope := acquisitionScope{contributions: []acquiredContribution{{ source: SourceInfo{ID: "outer-source"}, destination: Keys("records"), nested: nested, }}} composed := &composedDocument{ applications: []historyApplication{outer}, sequenceRanges: []sequenceRange{{ sourceIndex: 0, path: Keys("records"), start: 0, end: 1, items: []sequenceItemProjection{{inputIndex: 0, outputIndex: 2}}, }}, origins: make(map[string]Origin), } projectNestedEvidence(composed, scope) if got, want := composed.applications[1].path.String(), "/records/2"; got != want { t.Fatalf("nested history application path = %q, want %q", got, want) } if origin := composed.origins["/records/2/settings/value"]; len(origin.Locations) != 1 || origin.Locations[0].SourceID != "nested-source" { t.Fatalf("nested origin = %#v", origin) } query := Keys("records").Index(2).Key("settings").Key("value") if operation, relevant := applicationOperationPath(composed.applications[1], query, nil); !relevant || operation.String() != query.String() { t.Fatalf("nested historical query = %s, %v", operation.String(), relevant) } replayed := parseCompositionDocument(t, "lower", "records: [{id: first}, {id: middle}, {id: last}]\n") for _, application := range composed.applications { applyHistoryApplication(&replayed, application, new(mappingIndex)) } if value, present := documentAtPath(replayed, query); !present || value.Text != "nested" { t.Fatalf("replayed nested value = %#v, %v", value, present) } composed.applications[1].path.segments[0].key = "detached" if got := nested.applications[0].path.String(); got != "" { t.Fatalf("projected nested history path aliases its source: %q", got) } } func TestDeferredInputCompositionUsesRetainedAcquisitionScopes(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - path: item.yml destination: [records] placement: sequence-item name: main `) itemPath := writeImportFile(t, directory, "item.yml", `configkit: imports: - path: helper.yml id: item `) helperPath := writeImportFile(t, directory, "helper.yml", "value: acquired\n") accounting := &importAccounting{} input, diagnostics := testAcquireInput(inputAcquisitionRequest{ input: loadInput{source: File(mainPath), imports: true, filesystem: &FilesystemContext{Root: directory}}, role: sourceVersionMain, directiveContext: rootDirectiveContextImportsOnly, deferAssembly: true, }, acquisitionPolicy{importLimits: defaultImportLimits()}, accounting) if len(diagnostics) != 0 { t.Fatalf("acquire input: %#v", diagnostics) } if input.composition == nil || input.composition.root != nil || len(input.composition.acquisition) != 1 { t.Fatalf("acquisition handoff was composed early: %#v", input.composition) } if len(input.sources) != 3 || input.sources[0].ID != "source-1" || input.sources[1].ID != "source-2" || input.sources[2].ID != "source-3" { t.Fatalf("acquired source order/identity = %#v", input.sources) } if _, err := os.Stat(itemPath); err != nil { t.Fatalf("item source disappeared before composition: %v", err) } if _, err := os.Stat(helperPath); err != nil { t.Fatalf("helper source disappeared before composition: %v", err) } composition, diagnostics := compositionForInputWithPolicy(input, sequencePolicy{}) if len(diagnostics) != 0 { t.Fatalf("compose acquired input: %#v", diagnostics) } if value, present := documentAtPath(composition.root, Keys("name")); !present || value.Text != "main" { t.Fatalf("main value = %#v, %v", value, present) } for path, want := range map[string]string{ "/records/0/id": "item", "/records/0/value": "acquired", } { value, present := documentAtPath(composition.root, mustParsePath(t, path)) if !present || value.Text != want { t.Errorf("composed %s = %#v, %v; want %q", path, value, present, want) } } if got, want := input.sources[1].Destination.String(), "/records"; got != want { t.Errorf("item destination = %q, want %q", got, want) } if got, want := input.sources[2].Destination.String(), "/records/0"; got != want { t.Errorf("helper destination = %q, want %q", got, want) } } func TestItemImportCanJoinAnAuthoredListUnderRegisteredComposition(t *testing.T) { t.Parallel() type config struct { Items []map[string]string `yaml:"items"` } directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - path: item.yml destination: [items] placement: sequence-item `) writeImportFile(t, directory, "item.yml", "configkit: {imports: [{path: helper.yml}]}\nname: imported\n") writeImportFile(t, directory, "helper.yml", "id: helper-id\n") input, diagnostics := testAcquireInput(inputAcquisitionRequest{ input: loadInput{source: File(mainPath), imports: true}, role: sourceVersionMain, directiveContext: rootDirectiveContextImportsOnly, }, acquisitionPolicy{importLimits: defaultImportLimits()}, nil) if len(diagnostics) != 0 { t.Fatalf("assemble item import: %#v", diagnostics) } authored := parseDocumentForDecode(t, "items: [{id: authored-id, name: authored}]\n") composed, diagnostics := composeDocumentsWithPolicy(mustCompileSequenceIdentityPolicy(t, []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("items")}, identityFields: []string{"id"}}}), authored, input.document) if len(diagnostics) != 0 { t.Fatalf("compose authored and imported lists: %#v", diagnostics) } items, ok := documentAtPath(composed.root, Keys("items")) if !ok || items.Kind != document.Sequence || len(items.Sequence) != 2 { t.Fatalf("composed items = %#v", items) } if id, ok := documentAtPath(composed.root, Keys("items").Index(1).Key("id")); !ok || id.Text != "helper-id" { t.Fatalf("imported helper ID = %#v, %v", id, ok) } if origin := composed.origins[Keys("items").Index(1).Key("id").String()]; len(origin.Locations) != 1 || origin.Locations[0].SourceID != "source-3" { t.Fatalf("registered composition origin = %#v", origin) } } func TestParseImportDestinationUsesLiteralStringSegments(t *testing.T) { t.Parallel() for _, test := range []struct { name string destination string want Path }{ {name: "omitted"}, {name: "empty sequence", destination: " destination: []\n"}, { name: "literal keys", destination: " destination: ['', '.', '..', 'a/b', '~', '0', configkit2, myextends]\n", want: Keys("", ".", "..", "a/b", "~", "0", "configkit2", "myextends"), }, } { t.Run(test.name, func(t *testing.T) { body := "configkit:\n imports:\n - path: child.yml\n" + test.destination declarations := parseImportSpecificationsYAML(t, body) if len(declarations) != 1 { t.Fatalf("declarations = %#v", declarations) } got := declarations[0].destination if got.String() != test.want.String() { t.Fatalf("destination = %q, want %q", got.String(), test.want.String()) } for index, segment := range got.segments { if segment.kind != pathSegmentKey { t.Errorf("segment %d kind = %v, want mapping key", index, segment.kind) } } }) } } func TestParseImportPlacementDefaultsPerDeclaration(t *testing.T) { t.Parallel() declarations := parseImportSpecificationsYAML(t, `configkit: imports: - path: mapping.yml - glob: records/*.yml placement: sequence-item `) if len(declarations) != 2 || declarations[0].placement != placementModeMapping || declarations[1].placement != placementModeSequenceItem { t.Fatalf("declaration placements = %#v", declarations) } } func TestParseImportDestinationRejectsInvalidShapesAndMembers(t *testing.T) { t.Parallel() for _, test := range []struct { name string value string wantPath string badMember string }{ {name: "null", value: "null", wantPath: "/configkit/imports/0/destination", badMember: "null"}, {name: "scalar string", value: "prompts", wantPath: "/configkit/imports/0/destination", badMember: "prompts"}, {name: "number", value: "3", wantPath: "/configkit/imports/0/destination", badMember: "3"}, {name: "non-string number", value: "[valid, 7]", wantPath: "/configkit/imports/0/destination/1", badMember: "7"}, {name: "non-string null", value: "[valid, null]", wantPath: "/configkit/imports/0/destination/1", badMember: "null"}, {name: "non-string boolean", value: "[valid, true]", wantPath: "/configkit/imports/0/destination/1", badMember: "true"}, {name: "non-string mapping", value: "[valid, {key: value}]", wantPath: "/configkit/imports/0/destination/1", badMember: "{key"}, {name: "reserved configkit", value: "[valid, configkit]", wantPath: "/configkit/imports/0/destination/1", badMember: "configkit"}, {name: "reserved extends", value: "[valid, extends]", wantPath: "/configkit/imports/0/destination/1", badMember: "extends"}, } { t.Run(test.name, func(t *testing.T) { body := "configkit:\n imports:\n - path: child.yml\n destination: " + test.value + "\n" parsed, diagnostics := parseImportSpecificationYAML(body) if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportInvalid || diagnostics[0].Path.String() != test.wantPath { t.Fatalf("parse = %#v, want import.invalid at %q", diagnostics, test.wantPath) } line := strings.Split(body, "\n")[3] wantColumn := strings.Index(line, test.badMember) + 1 if diagnostics[0].Primary == nil || diagnostics[0].Primary.SourceID != "source-1" || diagnostics[0].Primary.Line != 4 || diagnostics[0].Primary.Column != wantColumn { t.Fatalf("invalid destination location = %#v, want line 4 column %d (parsed %v)", diagnostics[0].Primary, wantColumn, parsed) } }) } } func TestImportDestinationValidationPrecedesOptionalSuppression(t *testing.T) { t.Parallel() for _, test := range []struct { name string body string path string }{ { name: "optional missing path", body: "configkit:\n imports:\n - path: absent.yml\n destination: null\n", path: "/configkit/imports/0/destination", }, { name: "optional unmatched glob", body: "configkit:\n imports:\n - glob: absent/*.yml\n destination: [extends]\n", path: "/configkit/imports/0/destination/0", }, } { t.Run(test.name, func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", test.body) loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportInvalid) if diagnostics := loadError.Diagnostics(); diagnostics[0].Path.String() != test.path { t.Fatalf("diagnostic path = %q, want %q", diagnostics[0].Path.String(), test.path) } }) } } func TestLoadImportMissingContainmentAndRegularFileRules(t *testing.T) { t.Run("optional missing", func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - path: missing.yml settings: {root: retained} `) result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true}) if err != nil || result.Config.Settings.Root != "retained" || len(result.Sources()) != 1 { t.Fatalf("optional result = %#v, %v", result, err) } }) tests := []struct { name string path string setup func(*testing.T, string) code Code }{ {name: "required missing", path: "missing.yml", code: CodeImportMissing}, {name: "parent traversal", path: "../outside.yml", setup: func(t *testing.T, directory string) { writeImportFile(t, filepath.Dir(directory), "outside.yml", "settings: {root: outside}\n") }, code: CodeImportOutsideRoot}, {name: "directory", path: "directory", setup: func(t *testing.T, directory string) { if err := os.Mkdir(filepath.Join(directory, "directory"), 0o755); err != nil { t.Fatal(err) } }, code: CodeSourceNotRegular}, {name: "absolute", path: "/tmp/configkit.yml", code: CodeImportInvalid}, {name: "network URL", path: "https://example.invalid/config.yml", code: CodeImportInvalid}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { directory := t.TempDir() if test.setup != nil { test.setup(t, directory) } required := "" if test.code == CodeImportMissing { required = "\n required: true" } mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports:\n - path: \""+test.path+"\""+required+"\n") requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, test.code) }) } } func TestImportDefaultsReachOrdinaryAndGroupedDeclarations(t *testing.T) { t.Parallel() required := true directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: child.yml}]}\n") writeImportFile(t, directory, "child.yml", "configkit: {imports: [{path: absent.yml}]}\n") _, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{ Imports: true, importDefaults: importDefaults{required: &required}, }) var loadError *LoadError if !errors.As(err, &loadError) || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != CodeImportMissing { t.Fatalf("ordinary transitive required default error = %#v", err) } basePath := writeImportFile(t, directory, "base.yml", "settings: {root: base}\n") underlayPath := writeImportFile(t, directory, "underlay.yml", "configkit: {imports: [{path: absent.yml}]}\n") _, err = loadGroupedWithSecretLookup(loadRequest[importTestConfig]{ main: loadInput{source: File(basePath)}, underlays: []loadGroup{{inputs: []loadInput{{source: File(underlayPath), imports: true}}}}, importDefaults: importDefaults{required: &required}, }, testSecretBinding(), func(string) (string, bool) { return "", false }) if !errors.As(err, &loadError) || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != CodeImportMissing { t.Fatalf("grouped underlay required default error = %#v", err) } } func TestLoadImportIdentityReportsCyclesAndRepeatedRoutes(t *testing.T) { t.Run("cycle", func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports: [{path: a.yml}]\n") writeImportFile(t, directory, "a.yml", "configkit:\n imports: [{path: b.yml}]\n") writeImportFile(t, directory, "b.yml", "configkit:\n imports: [{path: main.yml}]\n") loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportCycle) diagnostic := loadError.Diagnostics()[0] if diagnostic.Primary == nil || len(diagnostic.Related) != 2 || !strings.Contains(diagnostic.Message, "main.yml") || !strings.Contains(diagnostic.Message, "a.yml") || !strings.Contains(diagnostic.Message, "b.yml") { t.Fatalf("cycle diagnostic = %#v", diagnostic) } }) t.Run("cycle through an item scope", func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - path: item.yml destination: [items] placement: sequence-item `) writeImportFile(t, directory, "item.yml", "configkit: {imports: [{path: main.yml}]}\n") loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportCycle) diagnostic := loadError.Diagnostics()[0] if diagnostic.Primary == nil || len(diagnostic.Related) != 1 || !strings.Contains(diagnostic.Message, "main.yml") || !strings.Contains(diagnostic.Message, "item.yml") { t.Fatalf("nested item cycle diagnostic = %#v", diagnostic) } }) t.Run("diamond", func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports: [{path: left.yml}, {path: right.yml}]\n") writeImportFile(t, directory, "left.yml", "configkit:\n imports: [{path: shared.yml}]\n") writeImportFile(t, directory, "right.yml", "configkit:\n imports: [{path: shared.yml}]\n") writeImportFile(t, directory, "shared.yml", "settings: {root: shared}\n") loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportRepeated) diagnostic := loadError.Diagnostics()[0] if diagnostic.Primary == nil || len(diagnostic.Related) < 3 { t.Fatalf("repeated-route diagnostic = %#v", diagnostic) } }) t.Run("symlink aliases", func(t *testing.T) { if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { t.Skip("initial filesystem contract covers Linux and macOS") } directory := t.TempDir() writeImportFile(t, directory, "target.yml", "settings: {root: target}\n") if err := os.Symlink("target.yml", filepath.Join(directory, "first-link.yml")); err != nil { t.Skipf("symlinks unavailable: %v", err) } if err := os.Symlink("target.yml", filepath.Join(directory, "second-link.yml")); err != nil { t.Skipf("symlinks unavailable: %v", err) } mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: first-link.yml}, {path: second-link.yml}]}\n") requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportRepeated) }) } func TestItemAcquisitionRetainsInclusionGraphAndScopeOwnership(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - path: item.yml destination: [items] placement: sequence-item `) itemPath := writeImportFile(t, directory, "item.yml", "configkit: {imports: [{path: helper.yml}]}\nname: item\n") helperPath := writeImportFile(t, directory, "helper.yml", "id: supplied\n") input, diagnostics := testAcquireInput(inputAcquisitionRequest{ input: loadInput{source: File(mainPath), imports: true}, role: sourceVersionMain, directiveContext: rootDirectiveContextImportsOnly, }, acquisitionPolicy{importLimits: defaultImportLimits()}, nil) if len(diagnostics) != 0 { t.Fatalf("assemble input: %#v", diagnostics) } graph := input.composition.importGraph if len(graph.nodes) != 3 || len(graph.edges) != 2 { t.Fatalf("inclusion graph has %d nodes and %d edges, want 3 and 2", len(graph.nodes), len(graph.edges)) } for index, want := range []struct { path string role importContributionRole }{ {path: canonicalTestPath(t, mainPath), role: importRoleMain}, {path: canonicalTestPath(t, itemPath), role: importRoleItem}, {path: canonicalTestPath(t, helperPath), role: importRoleMapping}, } { node := graph.nodes[index] if node.canonical != want.path || node.role != want.role || node.sourceID != "source-"+strconv.Itoa(index+1) { t.Errorf("graph node %d = %#v, want %s, %q, source-%d", index, node, want.role, want.path, index+1) } } rootScope := input.composition.acquisition[0] if rootScope.owner != graph.nodes[0].key || len(rootScope.contributions) != 2 { t.Fatalf("root acquisition scope = %#v", rootScope) } var item *acquiredContribution for index := range rootScope.contributions { if rootScope.contributions[index].role == importRoleItem { item = &rootScope.contributions[index] break } } if item == nil || item.identity != graph.nodes[1].key || item.scopeOwner != rootScope.owner || item.nested == nil { t.Fatalf("outer item contribution = %#v", item) } itemScope := item.nested.acquisition[0] if itemScope.owner != item.identity || len(itemScope.contributions) != 2 { t.Fatalf("nested item scope = %#v", itemScope) } for _, contribution := range itemScope.contributions { if contribution.scopeOwner != item.identity { t.Errorf("nested contribution scope owner = %#v, want item %v", contribution.scopeOwner, item.identity) } } if graph.edges[0].importer != graph.nodes[0].key || graph.edges[0].imported != graph.nodes[1].key || graph.edges[0].location.SourceID != "source-1" || graph.edges[1].importer != graph.nodes[1].key || graph.edges[1].imported != graph.nodes[2].key || graph.edges[1].location.SourceID != "source-2" { t.Fatalf("inclusion edges = %#v", graph.edges) } } func TestItemContributionsUseDependencyReadySchedules(t *testing.T) { t.Run("outer independent items", func(t *testing.T) { type item struct { ID string `yaml:"id"` } type config struct { Items []item `yaml:"items"` } definition := Definition[config]{sequenceIdentityRegistrations: []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("items")}, identityFields: []string{"id"}}}} directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - path: z-item.yml destination: [items] placement: sequence-item - path: a-item.yml destination: [items] placement: sequence-item items: [] `) zPath := writeImportFile(t, directory, "z-item.yml", "id: z\n") aPath := writeImportFile(t, directory, "a-item.yml", "id: a\n") result, err := testLoad[config](File(mainPath), Options[config]{Imports: true, Definition: definition}) if err != nil { t.Fatalf("load item imports: %v", err) } if len(result.Config.Items) != 2 || result.Config.Items[0].ID != "a" || result.Config.Items[1].ID != "z" { t.Fatalf("scheduled items = %#v", result.Config.Items) } wantSources := []string{canonicalTestPath(t, mainPath), canonicalTestPath(t, zPath), canonicalTestPath(t, aPath)} for index, want := range wantSources { if got := result.Sources()[index].Path; got != want { t.Errorf("acquired source %d = %q, want %q", index, got, want) } } }) t.Run("unequal helper branches", func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - path: item.yml destination: [items] placement: sequence-item `) writeImportFile(t, directory, "item.yml", `configkit: imports: - path: b-parent.yml - path: c-independent.yml id: item `) writeImportFile(t, directory, "b-parent.yml", "configkit: {imports: [{path: a-dependency.yml}]}\nid: b\n") writeImportFile(t, directory, "a-dependency.yml", "id: a\n") writeImportFile(t, directory, "c-independent.yml", "id: c\n") loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeComposeConflict) diagnostic := loadError.Diagnostics()[0] if diagnostic.Path.String() != "/items/0/id" || diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-4" { t.Fatalf("item helper conflict = %#v", diagnostic) } wantRelated := map[string]bool{"source-3": false, "source-5": false, "source-2": false} if len(diagnostic.Related) != len(wantRelated) { t.Fatalf("related sites = %#v", diagnostic.Related) } for _, site := range diagnostic.Related { if _, expected := wantRelated[site.SourceID]; !expected { t.Errorf("unexpected related site source %q", site.SourceID) } wantRelated[site.SourceID] = true } for sourceID, found := range wantRelated { if !found { t.Errorf("related sites do not include %q", sourceID) } } }) t.Run("selected main item dependencies", func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: helper.yml}]}\nid: main\n") writeImportFile(t, directory, "helper.yml", "id: helper\n") options := Options[importTestConfig]{ Imports: true, destination: Keys("items"), placementMode: placementModeSequenceItem, } loadError := requireLoadCode(t, File(mainPath), options, CodeComposeConflict) diagnostic := loadError.Diagnostics()[0] if diagnostic.Path.String() != "/items/0/id" || diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-2" || len(diagnostic.Related) != 1 || diagnostic.Related[0].SourceID != "source-1" { t.Fatalf("main item helper conflict = %#v", diagnostic) } }) } func TestBroadItemScopesKeepHelperOriginsAndDestinations(t *testing.T) { t.Parallel() type item struct { ID string `yaml:"id"` Value string `yaml:"value"` } type config struct { Items []item `yaml:"items"` } directory := t.TempDir() const count = 32 var main strings.Builder main.WriteString("configkit:\n imports:\n") for index := count - 1; index >= 0; index-- { name := strconv.Itoa(100 + index) main.WriteString(" - path: item-" + name + ".yml\n destination: [items]\n placement: sequence-item\n") writeImportFile(t, directory, "item-"+name+".yml", "configkit: {imports: [{path: helper-"+name+".yml}]}\nid: item-"+name+"\n") writeImportFile(t, directory, "helper-"+name+".yml", "value: helper-"+name+"\n") } mainPath := writeImportFile(t, directory, "main.yml", main.String()) definition := Definition[config]{sequenceIdentityRegistrations: []sequenceIdentityRegistration{{ selector: sequenceIdentitySelector{literalSequenceSelector("items")}, identityFields: []string{"id"}, }}} for _, layered := range []bool{false, true} { t.Run(strconv.FormatBool(layered), func(t *testing.T) { var loaded *Loaded[config] var err error if layered { loaded, err = LoadLayered(Input{Source: NoSource()}, testSecretBinding(), LayeredOptions[config]{ Definition: definition, Underlays: []Underlay{{Inputs: []Input{{Source: File(mainPath), Imports: true}}}}, }) } else { loaded, err = Load(File(mainPath), testSecretBinding(), Options[config]{Imports: true, Definition: definition}) } if err != nil { t.Fatal(err) } result := testLoaded(loaded) sources := result.Sources() if len(result.Config.Items) != count || len(sources) != 2*count+1 { t.Fatalf("item/source counts = %d/%d", len(result.Config.Items), len(sources)) } metadata := result.Metadata() for index, record := range result.Config.Items { name := strconv.Itoa(100 + index) if record.ID != "item-"+name || record.Value != "helper-"+name { t.Fatalf("item %d = %#v", index, record) } // Acquisition remains authored (descending); final item order is // dependency-ready (ascending), with helper evidence reprojected. helper := sources[2*(count-index)] field, found := metadata.Lookup(Keys("items").Index(index).Key("value")) if !found || len(field.Origin.Locations) != 1 || field.Origin.Locations[0].SourceID != helper.ID || helper.Path != canonicalTestPath(t, filepath.Join(directory, "helper-"+name+".yml")) || helper.Destination.String() != Keys("items").Index(index).String() { t.Fatalf("item %d helper evidence = %#v, %#v", index, field, helper) } } }) } } func TestItemHelperScopeScheduleInterleavesUnequalBranches(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - path: item.yml destination: [items] placement: sequence-item `) writeImportFile(t, directory, "item.yml", "configkit: {imports: [{path: b-parent.yml}, {path: c-independent.yml}]}\nitem: true\n") writeImportFile(t, directory, "b-parent.yml", "configkit: {imports: [{path: a-dependency.yml}]}\nb: true\n") writeImportFile(t, directory, "a-dependency.yml", "a: true\n") writeImportFile(t, directory, "c-independent.yml", "c: true\n") input, diagnostics := testAcquireInput(inputAcquisitionRequest{ input: loadInput{source: File(mainPath), imports: true}, role: sourceVersionMain, directiveContext: rootDirectiveContextImportsOnly, }, acquisitionPolicy{importLimits: defaultImportLimits()}, nil) if len(diagnostics) != 0 { t.Fatalf("assemble item scope: %#v", diagnostics) } var nested *composedDocument for _, contribution := range input.composition.acquisition[0].contributions { if contribution.role == importRoleItem { nested = contribution.nested break } } if nested == nil || len(nested.acquisition) != 1 { t.Fatalf("item composition = %#v", nested) } wantIDs := []string{"source-4", "source-3", "source-5", "source-2"} for index, want := range wantIDs { if got := nested.acquisition[0].contributions[index].source.ID; got != want { t.Errorf("local contribution %d = %q, want %q", index, got, want) } } } func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) { if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { t.Skip("initial filesystem contract covers Linux and macOS") } t.Run("contained absolute symlink", func(t *testing.T) { directory := t.TempDir() target := writeImportFile(t, directory, "target.yml", "settings: {first: linked}\n") if err := os.Symlink(target, filepath.Join(directory, "link.yml")); err != nil { t.Skipf("symlinks unavailable: %v", err) } mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: link.yml}]}\n") result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true}) if err != nil || result.Config.Settings.First != "linked" || result.Sources()[1].Path != canonicalTestPath(t, target) { t.Fatalf("symlink result = %#v, %v", result, err) } }) t.Run("symlink escape", func(t *testing.T) { root := t.TempDir() outside := t.TempDir() target := writeImportFile(t, outside, "outside.yml", "settings: {root: outside}\n") if err := os.Symlink(target, filepath.Join(root, "escape.yml")); err != nil { t.Skipf("symlinks unavailable: %v", err) } mainPath := writeImportFile(t, root, "main.yml", "configkit: {imports: [{path: escape.yml}]}\n") requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportOutsideRoot) }) t.Run("hard links are path identities", func(t *testing.T) { directory := t.TempDir() one := writeImportFile(t, directory, "one.yml", "settings: {first: one}\n") if err := os.Link(one, filepath.Join(directory, "two.yml")); err != nil { t.Skipf("hard links unavailable: %v", err) } mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: one.yml}, {path: two.yml}]}\n") requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeComposeConflict) }) t.Run("canonicalization failure", func(t *testing.T) { directory := t.TempDir() if err := os.Symlink("loop.yml", filepath.Join(directory, "loop.yml")); err != nil { t.Skipf("symlinks unavailable: %v", err) } mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: loop.yml}]}\n") requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportInvalid) }) } func TestLoadByteAndReaderImportsRequireExplicitFilesystemContext(t *testing.T) { directory := t.TempDir() writeImportFile(t, directory, "fragment.yml", "settings: {first: imported}\n") input := []byte("configkit: {imports: [{path: fragment.yml}]}\n") context := &FilesystemContext{BaseDir: directory, Root: directory} for _, test := range []struct { name string source func() Source }{ {name: "bytes URI label", source: func() Source { return Bytes("s3://bucket/main.yml", input) }}, {name: "reader URI label", source: func() Source { return Reader("https://example.invalid/main.yml", strings.NewReader(string(input))) }}, } { t.Run(test.name, func(t *testing.T) { result, err := testLoad[importTestConfig](test.source(), Options[importTestConfig]{Imports: true, Filesystem: context}) if err != nil || result.Config.Settings.First != "imported" { t.Fatalf("explicit filesystem result = %#v, %v", result, err) } }) } requireLoadCode(t, Bytes("memory.yml", input), Options[importTestConfig]{Imports: true}, CodeOptionsInvalid) requireLoadCode(t, Bytes("memory.yml", input), Options[importTestConfig]{Imports: true, Filesystem: &FilesystemContext{BaseDir: "relative", Root: directory}}, CodeOptionsInvalid) requireLoadCode(t, Bytes("memory.yml", input), Options[importTestConfig]{Imports: true, Filesystem: &FilesystemContext{BaseDir: directory, Root: t.TempDir()}}, CodeImportOutsideRoot) } func TestFileImportRootCanContainTheSelectedFileDirectory(t *testing.T) { root := t.TempDir() mainPath := writeImportFile(t, root, "configs/main.yml", "configkit: {imports: [{path: ../shared.yml}]}\n") writeImportFile(t, root, "shared.yml", "settings: {root: shared}\n") result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{ Imports: true, Filesystem: &FilesystemContext{Root: root}, }) if err != nil || result.Config.Settings.Root != "shared" { t.Fatalf("explicit-root result = %#v, %v", result, err) } requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true, Filesystem: &FilesystemContext{BaseDir: filepath.Dir(mainPath), Root: root}}, CodeOptionsInvalid) } func TestImportedParseAndDecodeDiagnosticsKeepTheirSource(t *testing.T) { t.Run("parse", func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: broken.yml}]}\n") writeImportFile(t, directory, "broken.yml", "settings: [\n") loadError := requireAnyLoadError(t, File(mainPath), Options[importTestConfig]{Imports: true}) diagnostics := loadError.Diagnostics() if diagnostics[0].Code != CodeYAMLInvalid || diagnostics[0].Primary == nil || diagnostics[0].Primary.SourceID != "source-2" || len(loadError.Sources()) != 2 { t.Fatalf("parse failure = diagnostics %#v, sources %#v", diagnostics, loadError.Sources()) } }) t.Run("decode", func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: unknown.yml}]}\n") writeImportFile(t, directory, "unknown.yml", "unknown: value\n") loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeDecodeUnknownField) diagnostic := loadError.Diagnostics()[0] if diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-2" { t.Fatalf("decode diagnostic = %#v", diagnostic) } }) } func TestOptionalImportDoesNotSuppressUnsafeOrUnreadableTargets(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "outside.yml", "configkit: {imports: [{path: ../missing.yml}]}\n") requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportOutsideRoot) locked := writeImportFile(t, directory, "locked.yml", "settings: {root: locked}\n") if err := os.Chmod(locked, 0); err != nil { t.Skipf("cannot create unreadable fixture: %v", err) } defer func() { _ = os.Chmod(locked, 0o600) }() if file, err := os.Open(locked); err == nil { _ = file.Close() t.Skip("filesystem does not enforce the unreadable-file fixture") } mainPath = writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: locked.yml}]}\n") requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeSourcePermission) } func TestImportTraversalEnforcesPrivateBudgets(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: one.yml}, {path: two.yml}]}\n") writeImportFile(t, directory, "one.yml", "settings: {first: one}\n") writeImportFile(t, directory, "two.yml", "settings: {second: two}\n") main := parseImportMain(t, mainPath) for _, test := range []struct { name string limits importLimits }{ {name: "source count", limits: importLimits{depth: 8, sources: 2, bytes: maxTotalBytes, nodes: maxDocumentNodes}}, {name: "aggregate bytes", limits: importLimits{depth: 8, sources: 8, bytes: main.bytes + 1, nodes: maxDocumentNodes}}, {name: "aggregate nodes", limits: importLimits{depth: 8, sources: 8, bytes: maxTotalBytes, nodes: countInputNodes(main.document) + 1}}, } { t.Run(test.name, func(t *testing.T) { request := testImportRequest(File(mainPath), nil, main, Path{}, nil, nil) _, _, diagnostics := testAcquireImportWithLimits(request, test.limits) if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportLimitExceeded { t.Fatalf("budget diagnostics = %#v", diagnostics) } }) } writeImportFile(t, directory, "one.yml", "configkit: {imports: [{path: nested.yml}]}\n") writeImportFile(t, directory, "nested.yml", "settings: {nested: nested}\n") main = parseImportMain(t, mainPath) request := testImportRequest(File(mainPath), nil, main, Path{}, nil, nil) _, _, diagnostics := testAcquireImportWithLimits(request, importLimits{depth: 1, sources: 8, bytes: maxTotalBytes, nodes: maxDocumentNodes}) if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportLimitExceeded { t.Fatalf("depth diagnostics = %#v", diagnostics) } } func TestImportDepthBudgetCountsSelectedEdges(t *testing.T) { for _, test := range []struct { name, declaration string code Code }{ {name: "no declaration"}, {name: "empty paths", declaration: "path: []"}, {name: "required empty paths", declaration: "path: []\n required: true"}, {name: "missing path", declaration: "path: absent.yml"}, {name: "missing directory", declaration: "dir: absent"}, {name: "missing glob", declaration: "glob: 'absent/*.yml'"}, {name: "selected path", declaration: "path: parts/next.yml", code: CodeImportLimitExceeded}, {name: "selected directory", declaration: "dir: parts", code: CodeImportLimitExceeded}, {name: "selected glob", declaration: "glob: 'parts/*.yml'", code: CodeImportLimitExceeded}, {name: "invalid empty declaration", declaration: "dir: []\n depth: 1", code: CodeImportInvalid}, {name: "invalid absent glob", declaration: "glob: '['", code: CodeImportInvalid}, } { t.Run(test.name, func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: leaf.yml}]}\n") leaf := "end: true\n" if test.declaration != "" { leaf += "configkit:\n imports:\n - " + test.declaration + "\n" } writeImportFile(t, directory, "leaf.yml", leaf) writeImportFile(t, directory, "parts/next.yml", "next: true\n") limits := defaultImportLimits() limits.depth = 1 main := parseImportMain(t, mainPath) request := testImportRequest(File(mainPath), nil, main, Path{}, nil, nil) composed, sources, diagnostics := testAcquireImportWithLimits(request, limits) if test.code != "" { if len(diagnostics) != 1 || diagnostics[0].Code != test.code { t.Fatalf("diagnostics = %#v, want %s", diagnostics, test.code) } return } if len(diagnostics) != 0 || composed == nil || len(sources) != 2 { t.Fatalf("depth-boundary composition: sources=%#v diagnostics=%#v", sources, diagnostics) } }) } } func TestImportTraversalAcceptsExactPrivateBudgets(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: imported.yml}]}\n") importedPath := writeImportFile(t, directory, "imported.yml", "settings: {first: imported}\n") main := parseImportMain(t, mainPath) imported := parseImportMain(t, importedPath) data, err := os.ReadFile(importedPath) if err != nil { t.Fatal(err) } limits := importLimits{ depth: 1, sources: 2, bytes: main.bytes + int64(len(data)), nodes: countInputNodes(main.document) + countInputNodes(imported.document), } request := testImportRequest(File(mainPath), nil, main, Path{}, nil, nil) composed, sources, diagnostics := testAcquireImportWithLimits(request, limits) if len(diagnostics) != 0 || composed == nil || len(sources) != 2 { t.Fatalf("exact-limit composition = (%#v, %#v, %#v)", composed, sources, diagnostics) } } func writeImportFile(t *testing.T, root, relative, contents string) string { t.Helper() filename := filepath.Join(root, filepath.FromSlash(relative)) if err := os.MkdirAll(filepath.Dir(filename), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filename, []byte(contents), 0o600); err != nil { t.Fatal(err) } return filename } func canonicalTestPath(t *testing.T, filename string) string { t.Helper() canonical, err := canonicalExistingPath(filename) if err != nil { t.Fatal(err) } return canonical } func requireLoadCode(t *testing.T, source Source, options Options[importTestConfig], code Code) *LoadError { t.Helper() loadError := requireAnyLoadError(t, source, options) diagnostics := loadError.Diagnostics() if len(diagnostics) == 0 || diagnostics[0].Code != code { t.Fatalf("diagnostics = %#v, want %s", diagnostics, code) } return loadError } func requireAnyLoadError(t *testing.T, source Source, options Options[importTestConfig]) *LoadError { t.Helper() result, err := testLoad[importTestConfig](source, options) if result != nil || err == nil { t.Fatalf("result, error = %#v, %v", result, err) } var loadError *LoadError if !errors.As(err, &loadError) { t.Fatalf("error type = %T", err) } return loadError } func parseImportMain(t *testing.T, filename string) parsedSource { t.Helper() parsed, diagnostics := parseSource(File(filename), "source-1", defaultParseLimits()) if len(diagnostics) != 0 { t.Fatalf("parse diagnostics = %#v", diagnostics) } return parsed } func testImportRequest(source Source, context *FilesystemContext, main parsedSource, destination Path, rule *VersionRule, processing *sourceVersionProcessingResult) importAcquisitionRequest { return importAcquisitionRequest{ input: inputAcquisitionRequest{ input: loadInput{ source: source, imports: true, filesystem: context, destination: destination.clone(), placementMode: main.source.placementMode, }, role: sourceVersionMain, directiveContext: rootDirectiveContextImportsOnly, protectionRule: rule, }, main: main, processing: processing, } } func testAcquireInput(request inputAcquisitionRequest, policy acquisitionPolicy, accounting *importAccounting) (assembledInput, []Diagnostic) { if accounting == nil { accounting = &importAccounting{} } state := &acquisitionState{accounting: accounting} tier := &tierAcquisitionState{visited: make(map[resourceIdentity]importRoute)} return acquireInput(request, policy, state, tier) } func testAcquireImport(request importAcquisitionRequest, policy acquisitionPolicy, state *acquisitionState, tier *tierAcquisitionState) (*composedDocument, []SourceInfo, []Diagnostic) { composition, sources, _, diagnostics := acquireImportedInput(request, &policy, state, tier) return composition, sources, diagnostics } func testAcquireImportWithLimits(request importAcquisitionRequest, limits importLimits) (*composedDocument, []SourceInfo, []Diagnostic) { return testAcquireImport(request, acquisitionPolicy{importLimits: limits}, &acquisitionState{accounting: &importAccounting{}}, &tierAcquisitionState{}) } func testAcquireImportWithAccounting(request importAcquisitionRequest, limits importLimits, accounting *importAccounting) (*composedDocument, []SourceInfo, []Diagnostic) { return testAcquireImport(request, acquisitionPolicy{importLimits: limits}, &acquisitionState{accounting: accounting}, &tierAcquisitionState{}) } func parseImportSpecificationsYAML(t *testing.T, input string) []importDeclarationSpec { t.Helper() declarations, diagnostics := parseImportSpecificationYAML(input) if len(diagnostics) != 0 { t.Fatalf("import parse diagnostics = %#v", diagnostics) } return declarations } func parseImportSpecificationYAML(input string) ([]importDeclarationSpec, []Diagnostic) { parsed, diagnostics := parseSource(Bytes("import fixture", []byte(input)), "source-1", defaultParseLimits()) if len(diagnostics) != 0 { return nil, diagnostics } _, declarations, diagnostics := splitImportDirectives(parsed.document) return declarations, diagnostics }