134 lines
4.7 KiB
Go
134 lines
4.7 KiB
Go
package engine
|
|
|
|
import (
|
|
"errors"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"testing/fstest"
|
|
)
|
|
|
|
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
|
|
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: "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)}
|
|
}
|
|
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)
|
|
})
|
|
}
|
|
}
|