Complete bounded glob imports
This commit is contained in:
273
glob_test.go
Normal file
273
glob_test.go
Normal file
@@ -0,0 +1,273 @@
|
||||
package configkit
|
||||
|
||||
import (
|
||||
"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 := Load[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 := Load[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 := Load[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, false)
|
||||
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", true)
|
||||
result, err := Load[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", true)
|
||||
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 := Load[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
||||
if err != nil || result.Config.Items["linked"] != "linked" {
|
||||
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", true)
|
||||
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", true)
|
||||
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", true)
|
||||
main := parseImportMain(t, mainPath)
|
||||
limits := defaultImportLimits()
|
||||
limits.entries = 2
|
||||
_, _, diagnostics := loadImportedDocuments(File(mainPath), nil, main, nil, 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
|
||||
_, _, diagnostics = loadImportedDocuments(File(mainPath), nil, main, nil, limits)
|
||||
if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportLimitExceeded {
|
||||
t.Fatalf("candidate-budget diagnostics = %#v", diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesystemContextIsValidatedWithoutImportsOrActiveProfile(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "items: {root: root}\n")
|
||||
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{
|
||||
Profile: "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 = Load[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 := Load[importTestConfig](NoSource(), Options[importTestConfig]{Profile: "default"})
|
||||
if err != nil || defaultsOnly == nil {
|
||||
t.Fatalf("default profile 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 := Load[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 writeGlobRoot(t *testing.T, directory, pattern string, optional bool) string {
|
||||
t.Helper()
|
||||
optionalLine := ""
|
||||
if optional {
|
||||
optionalLine = "\n optional: true"
|
||||
}
|
||||
return writeImportFile(t, directory, "main.yml", "_configkit:\n imports:\n - glob: '"+pattern+"'"+optionalLine+"\n")
|
||||
}
|
||||
Reference in New Issue
Block a user