323 lines
13 KiB
Go
323 lines
13 KiB
Go
package engine
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"runtime"
|
|
"testing"
|
|
)
|
|
|
|
func TestLoadGlobImportsUseLexicalDeclarationOrderAndNestedDFS(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
|
imports:
|
|
- glob: fragments/*.yml
|
|
- path: after.yml
|
|
items: {root: root}
|
|
`)
|
|
aPath := writeImportFile(t, directory, "fragments/a.yml", `configkit:
|
|
imports: [{path: nested/child.yml}]
|
|
items: {alpha: alpha}
|
|
`)
|
|
childPath := writeImportFile(t, directory, "fragments/nested/child.yml", "items: {child: child}\n")
|
|
bPath := writeImportFile(t, directory, "fragments/b.yml", "items: {beta: beta}\n")
|
|
afterPath := writeImportFile(t, directory, "after.yml", "items: {after: after}\n")
|
|
|
|
var previous []SourceInfo
|
|
for run := 0; run < 2; run++ {
|
|
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantItems := map[string]string{"root": "root", "alpha": "alpha", "child": "child", "beta": "beta", "after": "after"}
|
|
if !reflect.DeepEqual(result.Config.Items, wantItems) {
|
|
t.Fatalf("items = %#v, want %#v", result.Config.Items, wantItems)
|
|
}
|
|
sources := result.Sources()
|
|
wantPaths := []string{
|
|
canonicalTestPath(t, mainPath), canonicalTestPath(t, aPath),
|
|
canonicalTestPath(t, childPath), canonicalTestPath(t, bPath),
|
|
canonicalTestPath(t, afterPath),
|
|
}
|
|
gotPaths := make([]string, len(sources))
|
|
for index, source := range sources {
|
|
gotPaths[index] = source.Path
|
|
}
|
|
if !reflect.DeepEqual(gotPaths, wantPaths) {
|
|
t.Fatalf("source paths = %#v, want %#v", gotPaths, wantPaths)
|
|
}
|
|
if run != 0 && !reflect.DeepEqual(sources, previous) {
|
|
t.Fatalf("repeated traversal changed sources: %#v != %#v", sources, previous)
|
|
}
|
|
previous = sources
|
|
}
|
|
}
|
|
|
|
func TestLoadGlobImportsSupportEscapesClassesAndExplicitConfD(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
|
imports:
|
|
- glob: 'conf.d/literal\*.yml'
|
|
- glob: 'conf.d/item-[ab].yml'
|
|
- glob: 'conf.d/question?.yml'
|
|
items: {root: root}
|
|
`)
|
|
writeImportFile(t, directory, "conf.d/literal*.yml", "items: {literal: literal}\n")
|
|
writeImportFile(t, directory, "conf.d/item-b.yml", "items: {b: b}\n")
|
|
writeImportFile(t, directory, "conf.d/item-a.yml", "items: {a: a}\n")
|
|
writeImportFile(t, directory, "conf.d/question1.yml", "items: {question: question}\n")
|
|
|
|
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if want := map[string]string{"root": "root", "literal": "literal", "a": "a", "b": "b", "question": "question"}; !reflect.DeepEqual(result.Config.Items, want) {
|
|
t.Fatalf("items = %#v, want %#v", result.Config.Items, want)
|
|
}
|
|
|
|
ignoredPath := writeImportFile(t, directory, "conf.d/not-discovered.yml", "unknown: value\n")
|
|
_ = ignoredPath
|
|
plainPath := writeImportFile(t, directory, "plain.yml", "items: {root: only}\n")
|
|
plain, err := testLoad[importTestConfig](File(plainPath), Options[importTestConfig]{Imports: true})
|
|
if err != nil || len(plain.Sources()) != 1 || plain.Config.Items["root"] != "only" {
|
|
t.Fatalf("implicit discovery result = %#v, %v", plain, err)
|
|
}
|
|
}
|
|
|
|
func TestLoadGlobImportsRejectInvalidAndUnmatchedPatterns(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
pattern string
|
|
code Code
|
|
}{
|
|
{name: "bad class", pattern: "empty/[", code: CodeImportInvalid},
|
|
{name: "recursive star segment", pattern: "empty/**/value.yml", code: CodeImportInvalid},
|
|
{name: "recursive adjacent stars", pattern: "empty/value**.yml", code: CodeImportInvalid},
|
|
{name: "trailing separator", pattern: "empty/", code: CodeImportInvalid},
|
|
{name: "absolute", pattern: "/tmp/*.yml", code: CodeImportInvalid},
|
|
{name: "URL", pattern: "https://example.invalid/*.yml", code: CodeImportInvalid},
|
|
{name: "missing parent escape", pattern: "../missing/*.yml", code: CodeImportOutsideRoot},
|
|
{name: "unmatched", pattern: "empty/*.yml", code: CodeImportMissing},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
directory := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(directory, "empty"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mainPath := writeGlobRoot(t, directory, test.pattern, test.code == CodeImportMissing)
|
|
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, test.code)
|
|
})
|
|
}
|
|
|
|
directory := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(directory, "empty"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mainPath := writeGlobRoot(t, directory, "empty/*.yml", false)
|
|
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
|
if err != nil || len(result.Sources()) != 1 {
|
|
t.Fatalf("optional unmatched glob = %#v, %v", result, err)
|
|
}
|
|
}
|
|
|
|
func TestLoadGlobImportsPreserveIdentityAndFileRules(t *testing.T) {
|
|
t.Run("overlapping declarations", func(t *testing.T) {
|
|
directory := t.TempDir()
|
|
writeImportFile(t, directory, "conf/value.yml", "items: {value: value}\n")
|
|
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
|
imports:
|
|
- glob: conf/*.yml
|
|
- glob: conf/value.*
|
|
`)
|
|
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportRepeated)
|
|
})
|
|
|
|
t.Run("matched directory", func(t *testing.T) {
|
|
directory := t.TempDir()
|
|
if err := os.MkdirAll(filepath.Join(directory, "conf/matched.yml"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mainPath := writeGlobRoot(t, directory, "conf/*.yml", false)
|
|
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeSourceNotRegular)
|
|
})
|
|
|
|
t.Run("hard links stay distinct", func(t *testing.T) {
|
|
directory := t.TempDir()
|
|
one := writeImportFile(t, directory, "conf/one.yml", "items: {same: value}\n")
|
|
if err := os.Link(one, filepath.Join(directory, "conf/two.yml")); err != nil {
|
|
t.Skipf("hard links unavailable: %v", err)
|
|
}
|
|
mainPath := writeGlobRoot(t, directory, "conf/*.yml", false)
|
|
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeComposeConflict)
|
|
})
|
|
}
|
|
|
|
func TestLoadGlobImportsConfineSymlinkDirectories(t *testing.T) {
|
|
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
|
|
t.Skip("initial filesystem contract covers Linux and macOS")
|
|
}
|
|
t.Run("contained absolute target", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
target := filepath.Join(root, "target")
|
|
writeImportFile(t, target, "value.yml", "items: {linked: linked}\n")
|
|
if err := os.Symlink(target, filepath.Join(root, "linked")); err != nil {
|
|
t.Skipf("symlinks unavailable: %v", err)
|
|
}
|
|
mainPath := writeGlobRoot(t, root, "linked/*.yml", false)
|
|
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
|
if err != nil || result.Config.Items["linked"] != "linked" {
|
|
var loadError *LoadError
|
|
if errors.As(err, &loadError) {
|
|
t.Fatalf("contained directory symlink = %#v, diagnostics %#v", result, loadError.Diagnostics())
|
|
}
|
|
t.Fatalf("contained directory symlink = %#v, %v", result, err)
|
|
}
|
|
})
|
|
|
|
t.Run("escaping target", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
outside := t.TempDir()
|
|
writeImportFile(t, outside, "value.yml", "items: {escaped: escaped}\n")
|
|
if err := os.Symlink(outside, filepath.Join(root, "linked")); err != nil {
|
|
t.Skipf("symlinks unavailable: %v", err)
|
|
}
|
|
mainPath := writeGlobRoot(t, root, "linked/*.yml", false)
|
|
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportOutsideRoot)
|
|
})
|
|
}
|
|
|
|
func TestOptionalGlobDoesNotSuppressDirectoryPermissionFailure(t *testing.T) {
|
|
directory := t.TempDir()
|
|
locked := filepath.Join(directory, "locked")
|
|
if err := os.Mkdir(locked, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chmod(locked, 0); err != nil {
|
|
t.Skipf("cannot create unreadable directory fixture: %v", err)
|
|
}
|
|
defer func() { _ = os.Chmod(locked, 0o700) }()
|
|
if opened, err := os.Open(locked); err == nil {
|
|
_ = opened.Close()
|
|
t.Skip("filesystem does not enforce the unreadable-directory fixture")
|
|
}
|
|
mainPath := writeGlobRoot(t, directory, "locked/*.yml", false)
|
|
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportPermission)
|
|
}
|
|
|
|
func TestGlobTraversalBoundsNonmatchesAndAcceptedCandidates(t *testing.T) {
|
|
directory := t.TempDir()
|
|
for _, name := range []string{"one.txt", "two.txt", "three.txt"} {
|
|
writeImportFile(t, directory, "many/"+name, "ignored\n")
|
|
}
|
|
mainPath := writeGlobRoot(t, directory, "many/*.yml", false)
|
|
main := parseImportMain(t, mainPath)
|
|
limits := defaultImportLimits()
|
|
limits.entries = 2
|
|
request := testImportRequest(File(mainPath), nil, main, Path{}, nil, nil)
|
|
_, _, diagnostics := testAcquireImportWithLimits(request, limits)
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportLimitExceeded {
|
|
t.Fatalf("entry-budget diagnostics = %#v", diagnostics)
|
|
}
|
|
|
|
for _, name := range []string{"a.yml", "b.yml"} {
|
|
writeImportFile(t, directory, "matches/"+name, "items: {"+name[:1]+": value}\n")
|
|
}
|
|
mainPath = writeGlobRoot(t, directory, "matches/*.yml", false)
|
|
main = parseImportMain(t, mainPath)
|
|
limits = defaultImportLimits()
|
|
limits.sources = 2
|
|
request = testImportRequest(File(mainPath), nil, main, Path{}, nil, nil)
|
|
_, _, diagnostics = testAcquireImportWithLimits(request, limits)
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportLimitExceeded {
|
|
t.Fatalf("candidate-budget diagnostics = %#v", diagnostics)
|
|
}
|
|
}
|
|
|
|
func TestFilesystemContextIsValidatedWithoutImportsOrActiveOverlay(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", "items: {root: root}\n")
|
|
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{
|
|
Overlay: "default", Filesystem: &FilesystemContext{Root: directory},
|
|
})
|
|
if err != nil || result.Config.Items["root"] != "root" {
|
|
t.Fatalf("file filesystem context = %#v, %v", result, err)
|
|
}
|
|
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Filesystem: &FilesystemContext{BaseDir: directory, Root: directory}}, CodeOptionsInvalid)
|
|
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Filesystem: &FilesystemContext{Root: filepath.Join(directory, "missing")}}, CodeImportInvalid)
|
|
|
|
input := Bytes("memory.yml", []byte("items: {root: memory}\n"))
|
|
result, err = testLoad[importTestConfig](input, Options[importTestConfig]{Filesystem: &FilesystemContext{BaseDir: directory, Root: directory}})
|
|
if err != nil || result.Config.Items["root"] != "memory" {
|
|
t.Fatalf("byte filesystem context = %#v, %v", result, err)
|
|
}
|
|
requireLoadCode(t, Bytes("memory.yml", []byte("{}\n")), Options[importTestConfig]{Filesystem: &FilesystemContext{BaseDir: directory}}, CodeOptionsInvalid)
|
|
defaultsOnly, err := testLoad[importTestConfig](NoSource(), Options[importTestConfig]{Overlay: "default"})
|
|
if err != nil || defaultsOnly == nil {
|
|
t.Fatalf("default overlay with NoSource = %#v, %v", defaultsOnly, err)
|
|
}
|
|
|
|
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
|
|
selectedLink := filepath.Join(directory, "selected-link.yml")
|
|
if err := os.Symlink(mainPath, selectedLink); err == nil {
|
|
linked, err := testLoad[importTestConfig](File(selectedLink), Options[importTestConfig]{Filesystem: &FilesystemContext{Root: directory}})
|
|
if err != nil || linked.Sources()[0].Path != canonicalTestPath(t, mainPath) {
|
|
t.Fatalf("selected symlink identity = %#v, %v", linked, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNoSourceImportsAndFilesystemContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
directory := t.TempDir()
|
|
for _, imports := range []bool{false, true} {
|
|
name := "imports disabled"
|
|
if imports {
|
|
name = "imports enabled"
|
|
}
|
|
t.Run(name, func(t *testing.T) {
|
|
result, err := testLoad[importTestConfig](NoSource(), Options[importTestConfig]{
|
|
Imports: imports, Filesystem: &FilesystemContext{Root: directory},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if sources := result.Sources(); len(sources) != 0 {
|
|
t.Fatalf("NoSource sources = %#v", sources)
|
|
}
|
|
if _, ok := result.Metadata().Lookup(Keys("items", "root")); ok {
|
|
t.Fatal("NoSource contributed explicit input")
|
|
}
|
|
})
|
|
}
|
|
|
|
outside := t.TempDir()
|
|
for _, test := range []struct {
|
|
name string
|
|
context FilesystemContext
|
|
code Code
|
|
}{
|
|
{name: "missing root", context: FilesystemContext{BaseDir: directory}, code: CodeOptionsInvalid},
|
|
{name: "relative root", context: FilesystemContext{Root: "."}, code: CodeOptionsInvalid},
|
|
{name: "missing root directory", context: FilesystemContext{Root: filepath.Join(directory, "missing")}, code: CodeImportInvalid},
|
|
{name: "base outside root", context: FilesystemContext{BaseDir: outside, Root: directory}, code: CodeImportOutsideRoot},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
requireLoadCode(t, NoSource(), Options[importTestConfig]{Filesystem: &test.context}, test.code)
|
|
})
|
|
}
|
|
}
|
|
|
|
func writeGlobRoot(t *testing.T, directory, pattern string, required bool) string {
|
|
t.Helper()
|
|
requiredLine := ""
|
|
if required {
|
|
requiredLine = "\n required: true"
|
|
}
|
|
return writeImportFile(t, directory, "main.yml", "configkit:\n imports:\n - glob: '"+pattern+"'"+requiredLine+"\n")
|
|
}
|