1084 lines
44 KiB
Go
1084 lines
44 KiB
Go
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 := assembleBaseInput[config](File(mainPath), Options[config]{
|
|
Imports: true,
|
|
sequencePolicy: sequencePolicy{identityFields: map[string]string{"/tasks": "id"}},
|
|
})
|
|
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},
|
|
}
|
|
contexts, diagnostic := contextsForAcquiredSources(File(mainPath), nil, nil, 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: [{optional: 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: "optional kind", body: "configkit: {imports: [{path: a.yml, optional: '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()}, 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
|
|
- path: absent/*.yml
|
|
destination: [items]
|
|
optional: true
|
|
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 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 := assembleBaseInput(File(mainPath), Options[config]{Imports: true})
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("assemble item import: %#v", diagnostics)
|
|
}
|
|
authored := parseDocumentForDecode(t, "items: [{id: authored-id, name: authored}]\n")
|
|
composed, diagnostics := composeDocumentsWithPolicy(sequencePolicy{identityFields: map[string]string{"/items": "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 := parseImportDeclarationsYAML(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 := parseImportDeclarationsYAML(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 := parseImportDeclarationYAML(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 optional: true\n destination: null\n",
|
|
path: "/configkit/imports/0/destination",
|
|
},
|
|
{
|
|
name: "optional unmatched glob",
|
|
body: "configkit:\n imports:\n - glob: absent/*.yml\n optional: true\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
|
|
optional: true
|
|
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)
|
|
}
|
|
mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports:\n - path: \""+test.path+"\"\n")
|
|
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, test.code)
|
|
})
|
|
}
|
|
}
|
|
|
|
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 := assembleBaseInput[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
|
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)
|
|
}
|
|
item := rootScope.contributions[1]
|
|
if item.identity != graph.nodes[1].key || item.role != importRoleItem || 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 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, optional: true}]}\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, optional: true}]}\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) {
|
|
_, _, diagnostics := loadImportedDocuments(File(mainPath), nil, main, nil, 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)
|
|
_, _, diagnostics := loadImportedDocuments(File(mainPath), nil, main, nil, importLimits{depth: 1, sources: 8, bytes: maxTotalBytes, nodes: maxDocumentNodes})
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportLimitExceeded {
|
|
t.Fatalf("depth diagnostics = %#v", 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),
|
|
}
|
|
composed, sources, diagnostics := loadImportedDocuments(File(mainPath), nil, main, nil, 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 parseImportDeclarationsYAML(t *testing.T, input string) []importDeclaration {
|
|
t.Helper()
|
|
declarations, diagnostics := parseImportDeclarationYAML(input)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("import parse diagnostics = %#v", diagnostics)
|
|
}
|
|
return declarations
|
|
}
|
|
|
|
func parseImportDeclarationYAML(input string) ([]importDeclaration, []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
|
|
}
|