184 lines
6.5 KiB
Go
184 lines
6.5 KiB
Go
package engine
|
|
|
|
import (
|
|
"errors"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
"testing/fstest"
|
|
)
|
|
|
|
func TestResourceContextsPreserveLiteralBackslashes(t *testing.T) {
|
|
const directory = `assets\literal`
|
|
const name = directory + `/text\file`
|
|
check := func(t *testing.T, filesystem fs.FS) {
|
|
t.Helper()
|
|
data, err := fs.ReadFile(filesystem, name)
|
|
if err != nil || string(data) != "content" {
|
|
t.Fatalf("read = %q, %v", data, err)
|
|
}
|
|
entries, err := fs.ReadDir(filesystem, directory)
|
|
if err != nil || len(entries) != 1 || entries[0].Name() != `text\file` {
|
|
t.Fatalf("entries = %v, %v", entries, err)
|
|
}
|
|
if _, err := filesystem.Open("../outside"); !errors.Is(err, fs.ErrInvalid) {
|
|
t.Fatalf("escape = %v", err)
|
|
}
|
|
}
|
|
t.Run("native", func(t *testing.T) {
|
|
check(t, rootedNativeFS{filesystem: fstest.MapFS{"bundle/" + name: &fstest.MapFile{Data: []byte("content")}}, root: "bundle"})
|
|
})
|
|
t.Run("host", func(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("backslash is a host separator on Windows; native case remains covered")
|
|
}
|
|
root := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(root, directory), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(root, filepath.FromSlash(name)), []byte("content"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
check(t, rootedHostFS{root: root})
|
|
})
|
|
}
|
|
|
|
type trackedHostRoot struct {
|
|
root *os.Root
|
|
closed *int
|
|
failOpen bool
|
|
}
|
|
|
|
func (r *trackedHostRoot) Open(name string) (*os.File, error) {
|
|
if r.failOpen {
|
|
return nil, fs.ErrPermission
|
|
}
|
|
return r.root.Open(name)
|
|
}
|
|
func (r *trackedHostRoot) Close() error {
|
|
(*r.closed)++
|
|
return r.root.Close()
|
|
}
|
|
|
|
func TestRootedHostFSClosesRootOnSuccessfulAndFailedOpen(t *testing.T) {
|
|
rootPath := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(rootPath, "resource.txt"), []byte("resource"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opened, closed := 0, 0
|
|
failOpen := false
|
|
filesystem := rootedHostFS{root: rootPath, openRoot: func(name string) (hostResourceRoot, error) {
|
|
root, err := os.OpenRoot(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opened++
|
|
return &trackedHostRoot{root: root, closed: &closed, failOpen: failOpen}, nil
|
|
}}
|
|
if _, err := filesystem.Open("../resource.txt"); !errors.Is(err, fs.ErrInvalid) || opened != 0 {
|
|
t.Fatalf("invalid path = %v; roots opened %d", err, opened)
|
|
}
|
|
if _, err := filesystem.Open("missing.txt"); !errors.Is(err, fs.ErrNotExist) || opened != 1 || closed != 1 {
|
|
t.Fatalf("missing resource = %v; opened %d, closed %d", err, opened, closed)
|
|
}
|
|
failOpen = true
|
|
if _, err := filesystem.Open("resource.txt"); !errors.Is(err, fs.ErrPermission) || opened != 2 || closed != 2 {
|
|
t.Fatalf("failed inner open = %v; opened %d, closed %d", err, opened, closed)
|
|
}
|
|
failOpen = false
|
|
file, err := filesystem.Open("resource.txt")
|
|
if err != nil || opened != 3 || closed != 2 {
|
|
t.Fatalf("resource open = %v; opened %d, closed %d", err, opened, closed)
|
|
}
|
|
if err := file.Close(); err != nil || closed != 3 {
|
|
t.Fatalf("resource close = %v; opened %d, closed %d", err, opened, closed)
|
|
}
|
|
if _, err := file.Read(make([]byte, 1)); err == nil {
|
|
t.Fatal("closed resource file remained readable")
|
|
}
|
|
directory, err := filesystem.Open(".")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reader, ok := directory.(fs.ReadDirFile)
|
|
if !ok {
|
|
t.Fatal("rooted host directory did not expose ReadDirFile")
|
|
}
|
|
entries, err := reader.ReadDir(-1)
|
|
if err != nil || len(entries) != 1 || entries[0].Name() != "resource.txt" {
|
|
t.Fatalf("directory entries = %#v, %v", entries, err)
|
|
}
|
|
if err := directory.Close(); err != nil || opened != 4 || closed != 4 {
|
|
t.Fatalf("directory close = %v; opened %d, closed %d", err, opened, closed)
|
|
}
|
|
}
|
|
|
|
func TestNativeResourceViewIsBorrowedAndRooted(t *testing.T) {
|
|
filesystem := &observedFilesystem{files: fstest.MapFS{
|
|
"bundle/config/resource.txt": &fstest.MapFile{Data: []byte("content")},
|
|
}}
|
|
view := rootedNativeFS{filesystem: filesystem, root: "bundle"}
|
|
if filesystem.opened != 0 {
|
|
t.Fatal("constructing a native resource view opened files")
|
|
}
|
|
data, err := fs.ReadFile(view, "config/resource.txt")
|
|
if err != nil || string(data) != "content" {
|
|
t.Fatalf("resource = %q, %v", data, err)
|
|
}
|
|
filesystem.assertClosed(t)
|
|
if _, err := fs.ReadFile(view, "../outside.txt"); !errors.Is(err, fs.ErrInvalid) {
|
|
t.Fatalf("resource escape = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadingClosesNativeFilesAcrossPipelineOutcomes(t *testing.T) {
|
|
type config struct {
|
|
Value string `yaml:"value"`
|
|
Version string `yaml:"version"`
|
|
}
|
|
for _, test := range []struct {
|
|
name string
|
|
main string
|
|
child string
|
|
helper string
|
|
failRead string
|
|
options Options[config]
|
|
wantErr bool
|
|
}{
|
|
{name: "success", main: "value: retained\n"},
|
|
{name: "acquisition", main: "value: retained\n", failRead: "main.yml", wantErr: true},
|
|
{name: "parse", main: "value: [broken\n", wantErr: true},
|
|
{name: "composition", main: "configkit: {imports: [{path: child.yml}]}\nvalue: first\n", child: "value: second\n", options: Options[config]{Imports: true}, wantErr: true},
|
|
{
|
|
name: "nested item composition",
|
|
main: "configkit: {imports: [{path: child.yml, destination: [children], placement: sequence-item}]}\nid: parent\n",
|
|
child: "configkit: {imports: [{path: helper.yml}]}\nid: child\nvalue: child\n",
|
|
helper: "value: helper\n",
|
|
options: Options[config]{
|
|
Imports: true, destination: Keys("groups"), placementMode: placementModeSequenceItem,
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{name: "version", main: "version: wrong\n", options: Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error { return fs.ErrInvalid }}}}, wantErr: true},
|
|
{name: "validation", main: "value: retained\n", options: Options[config]{Definition: Definition[config]{Validate: func(_ *config, validation *Validation) { validation.Error(Keys("value"), "rejected") }}}, wantErr: true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
files := fstest.MapFS{"main.yml": &fstest.MapFile{Data: []byte(test.main)}}
|
|
if test.child != "" {
|
|
files["child.yml"] = &fstest.MapFile{Data: []byte(test.child)}
|
|
}
|
|
if test.helper != "" {
|
|
files["helper.yml"] = &fstest.MapFile{Data: []byte(test.helper)}
|
|
}
|
|
filesystem := &observedFilesystem{files: files, failRead: test.failRead}
|
|
_, err := testLoad(filesystemFile(newNativeNamespace("bundle", filesystem), "main.yml"), test.options)
|
|
if (err != nil) != test.wantErr {
|
|
t.Fatalf("load error = %v, want failure %v", err, test.wantErr)
|
|
}
|
|
filesystem.assertClosed(t)
|
|
})
|
|
}
|
|
}
|