ollie/fsedsl/decl_test.go

1363 lines
35 KiB
Go

package fsedsl
import (
"context"
"errors"
"os"
"testing"
)
// TestCtx is a simple test context.
type TestCtx struct {
Data any
UserID string
}
// ══════════════════════════════════════════════════════════════════
// NodeOption tests
// ══════════════════════════════════════════════════════════════════
func TestNodeOptions(t *testing.T) {
// Test Stream option
t.Run("Stream", func(t *testing.T) {
streamFn := func(ctx TestCtx, cctx context.Context, base string) ([]byte, string, error) {
return nil, "", nil
}
spec := FileNode[TestCtx]("test", 0444, Stream(streamFn))
if spec.Stream == nil {
t.Error("Stream not set")
}
})
// Test BlockOnce option
t.Run("BlockOnce", func(t *testing.T) {
blockFn := func(ctx TestCtx, cctx context.Context, base string) ([]byte, string, error) {
return nil, "", nil
}
spec := FileNode[TestCtx]("test", 0444, BlockOnce(blockFn))
if spec.BlockOnce == nil {
t.Error("BlockOnce not set")
}
})
// Test Request option
t.Run("Request", func(t *testing.T) {
reqFn := func(ctx TestCtx, data []byte) ([]byte, error) {
return nil, nil
}
spec := FileNode[TestCtx]("test", 0666, Request(reqFn))
if spec.Request == nil {
t.Error("Request not set")
}
})
// Test RemoveNode option
t.Run("RemoveNode", func(t *testing.T) {
removeFn := func(ctx TestCtx) error {
return nil
}
spec := DirNode[TestCtx]("test",
FileNode[TestCtx]("child", 0444),
RemoveNode[TestCtx](removeFn),
)
if spec.Remove == nil {
t.Error("Remove not set")
}
})
// Test StatOverride option
t.Run("StatOverride", func(t *testing.T) {
statFn := func() os.FileInfo {
return &SyntheticFileInfo{Name_: "test"}
}
spec := FileNode[TestCtx]("test", 0444, StatOverride[TestCtx](statFn))
if spec.Stat == nil {
t.Error("Stat not set")
}
})
// Test UID option
t.Run("UID", func(t *testing.T) {
spec := FileNode[TestCtx]("test", 0444, UID[TestCtx]("owner"))
if spec.UID != "owner" {
t.Errorf("UID = %q, want 'owner'", spec.UID)
}
})
// Test GID option
t.Run("GID", func(t *testing.T) {
spec := FileNode[TestCtx]("test", 0444, GID[TestCtx]("group"))
if spec.GID != "group" {
t.Errorf("GID = %q, want 'group'", spec.GID)
}
})
// Test Alias option
// Test Bind option
t.Run("Bind", func(t *testing.T) {
spec := DirNode[TestCtx]("test",
FileNode[TestCtx]("child", 0444),
Bind[TestCtx](BindData),
)
if len(spec.BindTargets) != 1 {
t.Errorf("BindTargets = %v, want 1 element", spec.BindTargets)
}
})
}
// ══════════════════════════════════════════════════════════════════
// Tree tests
// ══════════════════════════════════════════════════════════════════
func TestTreeMount(t *testing.T) {
parent := newTree(nil, 0755)
child := newTree(nil, 0755,
withLister(func(_ []string) ([]os.DirEntry, error) {
return []os.DirEntry{fileEntry("childfile", 0444)}, nil
}),
)
parent.Mount("subdir", child)
// Test Child retrieval
got := parent.Child("subdir")
if got != child {
t.Error("Child did not return mounted child")
}
// Test Children map
children := parent.Children()
if len(children) != 1 {
t.Errorf("Children = %d, want 1", len(children))
}
if children["subdir"] != child {
t.Error("Children map missing 'subdir'")
}
// Test nil child
nilChild := parent.Child("nonexistent")
if nilChild != nil {
t.Error("Child should return nil for nonexistent")
}
}
func TestTreeSplit(t *testing.T) {
parent := newTree(nil, 0755)
child := newTree(nil, 0755)
parent.Mount("sub", child)
// Test exact match
c, rest, ok := parent.split("sub")
if !ok {
t.Error("split should find 'sub'")
}
if c != child {
t.Error("split returned wrong child")
}
if rest != "" {
t.Errorf("rest = %q, want ''", rest)
}
// Test prefix match
c2, rest2, ok2 := parent.split("sub/file")
if !ok2 {
t.Error("split should find 'sub/file'")
}
if c2 != child {
t.Error("split returned wrong child")
}
if rest2 != "file" {
t.Errorf("rest = %q, want 'file'", rest2)
}
// Test no match
_, _, ok3 := parent.split("other")
if ok3 {
t.Error("split should not find 'other'")
}
// Test tree without children
empty := newTree(nil, 0755)
_, _, ok4 := empty.split("anything")
if ok4 {
t.Error("split should not find anything in empty tree")
}
}
func TestTreeIndexOf(t *testing.T) {
// Test indexOf helper
if indexOf("hello/world", '/') != 5 {
t.Error("indexOf failed for slash")
}
if indexOf("noslash", '/') != -1 {
t.Error("indexOf should return -1")
}
}
// ══════════════════════════════════════════════════════════════════
// fileConfig tests
// ══════════════════════════════════════════════════════════════════
func TestFileConfigMethods(t *testing.T) {
fc := &fileConfig{
StatFn: func() (os.FileInfo, error) { return &SyntheticFileInfo{Name_: "test"}, nil },
ReadFn: func() ([]byte, error) { return []byte("content"), nil },
WriteFn: func(data []byte) error { return nil },
BlockingReadFn: func(ctx context.Context, base string) ([]byte, string, error) { return nil, "", nil },
}
// Stat
fi, err := fc.Stat()
if err != nil || fi.Name() != "test" {
t.Error("Stat failed")
}
// Read
data, err := fc.Read()
if err != nil || string(data) != "content" {
t.Error("Read failed")
}
// Write
err = fc.Write([]byte("x"))
if err != nil {
t.Error("Write failed")
}
// BlockingRead
_, _, err = fc.BlockingRead(context.Background(), "")
if err != nil {
t.Error("BlockingRead failed")
}
}
func TestFileConfigModes(t *testing.T) {
// Default: all false
fc := &fileConfig{}
if fc.BlockingReadMode() || fc.StreamMode() || fc.RequestRespMode() {
t.Error("default modes should be false")
}
// BlockingReadMode
fc2 := &fileConfig{BlockingRead_: true}
if !fc2.BlockingReadMode() {
t.Error("BlockingReadMode should be true")
}
// StreamMode
fc3 := &fileConfig{StreamMode_: true}
if !fc3.StreamMode() {
t.Error("StreamMode should be true")
}
// RequestRespMode
fc4 := &fileConfig{RequestResp_: true}
if !fc4.RequestRespMode() {
t.Error("RequestRespMode should be true")
}
}
// ══════════════════════════════════════════════════════════════════
// SyntheticFileInfo tests
// ══════════════════════════════════════════════════════════════════
func TestSyntheticFileInfo(t *testing.T) {
fi := &SyntheticFileInfo{
Name_: "testfile",
Mode_: 0644,
Size_: 100,
IsDir_: false,
UID_: "user",
GID_: "group",
Vers_: 1,
}
if fi.Name() != "testfile" {
t.Errorf("Name = %q", fi.Name())
}
if fi.Size() != 100 {
t.Errorf("Size = %d", fi.Size())
}
if fi.Mode() != 0644 {
t.Errorf("Mode = %o", fi.Mode())
}
if !fi.ModTime().IsZero() {
t.Error("ModTime should be zero")
}
if fi.IsDir() {
t.Error("IsDir should be false")
}
if fi.Sys() != nil {
t.Error("Sys should be nil")
}
// Test directory
fiDir := &SyntheticFileInfo{IsDir_: true}
if !fiDir.IsDir() {
t.Error("IsDir should be true")
}
}
// ══════════════════════════════════════════════════════════════════
// syntheticEntry tests
// ══════════════════════════════════════════════════════════════════
func TestSyntheticEntry(t *testing.T) {
// File entry
fe := &syntheticEntry{name: "file", mode: 0644, isDir: false}
if fe.Name() != "file" {
t.Errorf("Name = %q", fe.Name())
}
if fe.IsDir() {
t.Error("IsDir should be false")
}
if fe.Type() != 0 {
t.Errorf("Type = %v, want 0", fe.Type())
}
fi, err := fe.Info()
if err != nil {
t.Fatalf("Info: %v", err)
}
if fi.Name() != "file" || fi.Mode() != 0644 {
t.Error("Info returned wrong values")
}
// Dir entry
de := &syntheticEntry{name: "dir", mode: 0755, isDir: true}
if !de.IsDir() {
t.Error("IsDir should be true")
}
if de.Type() != os.ModeDir {
t.Errorf("Type = %v, want ModeDir", de.Type())
}
}
func TestFileEntryAndDirEntry(t *testing.T) {
fe := fileEntry("myfile", 0644)
if fe.Name() != "myfile" || fe.IsDir() {
t.Error("fileEntry wrong")
}
de := dirEntry("mydir", 0755)
if de.Name() != "mydir" || !de.IsDir() {
t.Error("DirEntry wrong")
}
}
// ══════════════════════════════════════════════════════════════════
// Builder tests - delete/rename operations
// ══════════════════════════════════════════════════════════════════
func TestDeleteDir(t *testing.T) {
var deleted bool
spec := DirNode[TestCtx]("/",
Each("{id}", func(ctx TestCtx) ([]Binding[TestCtx], error) {
return []Binding[TestCtx]{{Name: "item"}}, nil
},
FileNode[TestCtx]("data", 0444),
RemoveNode[TestCtx](func(ctx TestCtx) error {
deleted = true
return nil
}),
),
)
tree := BuildTree(spec, TestCtx{})
err := tree.Delete("item")
if err != nil {
t.Fatalf("Delete: %v", err)
}
if !deleted {
t.Error("remove handler not called")
}
}
func TestDeleteDirNotFound(t *testing.T) {
spec := DirNode[TestCtx]("/",
FileNode[TestCtx]("file", 0444),
)
tree := BuildTree(spec, TestCtx{})
err := tree.Delete("nonexistent")
if err == nil {
t.Error("Delete should fail for nonexistent")
}
}
func TestDeleteDirNotRemovable(t *testing.T) {
spec := DirNode[TestCtx]("/",
Each("{id}", func(ctx TestCtx) ([]Binding[TestCtx], error) {
return []Binding[TestCtx]{{Name: "item"}}, nil
},
FileNode[TestCtx]("data", 0444),
// No RemoveNode set
),
)
tree := BuildTree(spec, TestCtx{})
err := tree.Delete("item")
if err == nil {
t.Error("Delete should fail when not removable")
}
}
func TestRenameDir(t *testing.T) {
var newName string
spec := DirNode[TestCtx]("/",
Each("{id}", func(ctx TestCtx) ([]Binding[TestCtx], error) {
return []Binding[TestCtx]{{
Name: "item",
Rename: func(n string) error {
newName = n
return nil
},
}}, nil
},
FileNode[TestCtx]("data", 0444),
),
)
tree := BuildTree(spec, TestCtx{})
err := tree.Rename("item", "newitem")
if err != nil {
t.Fatalf("Rename: %v", err)
}
if newName != "newitem" {
t.Errorf("newName = %q, want 'newitem'", newName)
}
}
func TestRenameDirNotFound(t *testing.T) {
spec := DirNode[TestCtx]("/",
FileNode[TestCtx]("file", 0444),
)
tree := BuildTree(spec, TestCtx{})
err := tree.Rename("nonexistent", "new")
if err == nil {
t.Error("Rename should fail for nonexistent")
}
}
func TestRenameDirNotSupported(t *testing.T) {
spec := DirNode[TestCtx]("/",
Each("{id}", func(ctx TestCtx) ([]Binding[TestCtx], error) {
return []Binding[TestCtx]{{Name: "item"}}, nil // No Rename handler
},
FileNode[TestCtx]("data", 0444),
),
)
tree := BuildTree(spec, TestCtx{})
err := tree.Rename("item", "new")
if err == nil {
t.Error("Rename should fail when not supported")
}
}
// ══════════════════════════════════════════════════════════════════
// Builder tests - blocking modes in openLeaf
// ══════════════════════════════════════════════════════════════════
func TestOpenLeafBlockOnce(t *testing.T) {
spec := DirNode[TestCtx]("/",
FileNode[TestCtx]("blocking", 0444,
BlockOnce(func(ctx TestCtx, cctx context.Context, base string) ([]byte, string, error) {
return []byte("data"), "next", nil
}),
),
)
tree := BuildTree(spec, TestCtx{})
f, err := tree.Open("blocking")
if err != nil {
t.Fatalf("Open: %v", err)
}
if !f.BlockingReadMode() {
t.Error("expected BlockingReadMode")
}
if f.StreamMode() || f.RequestRespMode() {
t.Error("other modes should be false")
}
}
func TestOpenLeafStream(t *testing.T) {
spec := DirNode[TestCtx]("/",
FileNode[TestCtx]("streaming", 0444,
Stream(func(ctx TestCtx, cctx context.Context, base string) ([]byte, string, error) {
return []byte("stream"), "cursor", nil
}),
),
)
tree := BuildTree(spec, TestCtx{})
f, err := tree.Open("streaming")
if err != nil {
t.Fatalf("Open: %v", err)
}
if !f.StreamMode() {
t.Error("expected StreamMode")
}
if f.BlockingReadMode() || f.RequestRespMode() {
t.Error("other modes should be false")
}
}
func TestOpenLeafRequest(t *testing.T) {
spec := DirNode[TestCtx]("/",
FileNode[TestCtx]("request", 0666,
Request(func(ctx TestCtx, data []byte) ([]byte, error) {
return append([]byte("echo:"), data...), nil
}),
),
)
tree := BuildTree(spec, TestCtx{})
f, err := tree.Open("request")
if err != nil {
t.Fatalf("Open: %v", err)
}
if !f.RequestRespMode() {
t.Error("expected RequestRespMode")
}
// Write then read
err = f.Write([]byte("test"))
if err != nil {
t.Fatalf("Write: %v", err)
}
data, err := f.Read()
if err != nil {
t.Fatalf("Read: %v", err)
}
if string(data) != "echo:test" {
t.Errorf("data = %q, want 'echo:test'", data)
}
}
func TestOpenLeafRequestError(t *testing.T) {
spec := DirNode[TestCtx]("/",
FileNode[TestCtx]("request", 0666,
Request(func(ctx TestCtx, data []byte) ([]byte, error) {
return nil, errors.New("request error")
}),
),
)
tree := BuildTree(spec, TestCtx{})
f, err := tree.Open("request")
if err != nil {
t.Fatalf("Open: %v", err)
}
err = f.Write([]byte("test"))
if err == nil {
t.Error("Write should fail when request returns error")
}
}
func TestOpenLeafRequestWithFallbackRead(t *testing.T) {
spec := DirNode[TestCtx]("/",
FileNode[TestCtx]("request", 0666,
Read(func(ctx TestCtx) ([]byte, error) {
return []byte("fallback"), nil
}),
Request(func(ctx TestCtx, data []byte) ([]byte, error) {
return []byte("response"), nil
}),
),
)
tree := BuildTree(spec, TestCtx{})
f, err := tree.Open("request")
if err != nil {
t.Fatalf("Open: %v", err)
}
// Read without write should use fallback
data, err := f.Read()
if err != nil {
t.Fatalf("Read: %v", err)
}
if string(data) != "fallback" {
t.Errorf("data = %q, want 'fallback'", data)
}
}
func TestOpenLeafNoWriteHandler(t *testing.T) {
spec := DirNode[TestCtx]("/",
FileNode[TestCtx]("readonly", 0444,
Read(func(ctx TestCtx) ([]byte, error) {
return []byte("data"), nil
}),
// No Write handler
),
)
tree := BuildTree(spec, TestCtx{})
f, err := tree.Open("readonly")
if err != nil {
t.Fatalf("Open: %v", err)
}
err = f.Write([]byte("test"))
if err == nil {
t.Error("Write should fail on read-only file")
}
}
func TestOpenLeafNoReadHandler(t *testing.T) {
spec := DirNode[TestCtx]("/",
FileNode[TestCtx]("noread", 0222,
Write(func(ctx TestCtx, data []byte) error {
return nil
}),
// No Read handler
),
)
tree := BuildTree(spec, TestCtx{})
f, err := tree.Open("noread")
if err != nil {
t.Fatalf("Open: %v", err)
}
// Read should return empty
data, err := f.Read()
if err != nil {
t.Fatalf("Read: %v", err)
}
if len(data) != 0 {
t.Errorf("data = %q, want empty", data)
}
}
func TestOpenLeafStatWithCustomStat(t *testing.T) {
spec := DirNode[TestCtx]("/",
FileNode[TestCtx]("custom", 0444,
StatOverride[TestCtx](func() os.FileInfo {
return &SyntheticFileInfo{Name_: "customname", Size_: 999, Mode_: 0777}
}),
),
)
tree := BuildTree(spec, TestCtx{})
f, err := tree.Open("custom")
if err != nil {
t.Fatalf("Open: %v", err)
}
fi, err := f.Stat()
if err != nil {
t.Fatalf("Stat: %v", err)
}
if fi.Name() != "customname" {
t.Errorf("Name = %q", fi.Name())
}
if fi.Size() != 999 {
t.Errorf("Size = %d", fi.Size())
}
}
func TestOpenLeafDefaultBlockingRead(t *testing.T) {
spec := DirNode[TestCtx]("/",
FileNode[TestCtx]("normal", 0444,
Read(func(ctx TestCtx) ([]byte, error) {
return []byte("data"), nil
}),
),
)
tree := BuildTree(spec, TestCtx{})
f, err := tree.Open("normal")
if err != nil {
t.Fatalf("Open: %v", err)
}
// BlockingRead should return error for non-blocking file
_, _, err = f.BlockingRead(context.Background(), "")
if err == nil {
t.Error("BlockingRead should fail for non-blocking file")
}
}
// ══════════════════════════════════════════════════════════════════
// Builder tests - validation
// ══════════════════════════════════════════════════════════════════
func TestValidateDirWithFileHandlers(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("should panic for dir with file handlers")
}
}()
spec := DirNode[TestCtx]("/",
FileNode[TestCtx]("child", 0444),
Read(func(ctx TestCtx) ([]byte, error) { return nil, nil }),
)
BuildTree(spec, TestCtx{})
}
func TestValidateMultipleBlockingModes(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("should panic for multiple blocking modes")
}
}()
spec := DirNode[TestCtx]("/",
FileNode[TestCtx]("file", 0444,
BlockOnce(func(ctx TestCtx, cctx context.Context, base string) ([]byte, string, error) { return nil, "", nil }),
Stream(func(ctx TestCtx, cctx context.Context, base string) ([]byte, string, error) { return nil, "", nil }),
),
)
BuildTree(spec, TestCtx{})
}
func TestValidateTemplateWithoutBindings(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("should panic for template without bindings")
}
}()
spec := DirNode[TestCtx]("/",
DirNode[TestCtx]("{id}",
FileNode[TestCtx]("data", 0444),
),
)
BuildTree(spec, TestCtx{})
}
// ══════════════════════════════════════════════════════════════════
// Builder tests - findChild with nested paths
// ══════════════════════════════════════════════════════════════════
func TestFindChildNestedPath(t *testing.T) {
spec := DirNode[TestCtx]("/",
DirNode[TestCtx]("level1",
DirNode[TestCtx]("level2",
FileNode[TestCtx]("file", 0444,
Read(func(ctx TestCtx) ([]byte, error) {
return []byte("deep"), nil
}),
),
),
),
)
tree := BuildTree(spec, TestCtx{})
// Open nested file
f, err := tree.Open("level1/level2/file")
if err != nil {
t.Fatalf("Open: %v", err)
}
data, err := f.Read()
if err != nil {
t.Fatalf("Read: %v", err)
}
if string(data) != "deep" {
t.Errorf("data = %q, want 'deep'", data)
}
}
func TestFindChildNestedNotFound(t *testing.T) {
spec := DirNode[TestCtx]("/",
DirNode[TestCtx]("level1",
FileNode[TestCtx]("file", 0444),
),
)
tree := BuildTree(spec, TestCtx{})
_, err := tree.Open("level1/nonexistent/file")
if err == nil {
t.Error("Open should fail for nonexistent nested path")
}
}
// ══════════════════════════════════════════════════════════════════
// Builder tests - Each with file handlers (file template)
// ══════════════════════════════════════════════════════════════════
func TestEachFileTemplate(t *testing.T) {
items := map[string]string{
"item1": "content1",
"item2": "content2",
}
spec := DirNode[TestCtx]("/",
Each("{id}", func(ctx TestCtx) ([]Binding[TestCtx], error) {
var bindings []Binding[TestCtx]
for name, content := range items {
c := content // capture
bindings = append(bindings, Binding[TestCtx]{
Name: name,
Applier: func(ctx TestCtx) TestCtx {
ctx.Data = c
return ctx
},
})
}
return bindings, nil
},
Read(func(ctx TestCtx) ([]byte, error) {
return []byte(ctx.Data.(string)), nil
}),
),
)
tree := BuildTree(spec, TestCtx{})
// List should show files
entries, err := tree.List()
if err != nil {
t.Fatalf("List: %v", err)
}
if len(entries) != 2 {
t.Errorf("entries = %d, want 2", len(entries))
}
for _, e := range entries {
if e.IsDir() {
t.Errorf("%s should be a file", e.Name())
}
}
// Open and read
f, err := tree.Open("item1")
if err != nil {
t.Fatalf("Open: %v", err)
}
data, err := f.Read()
if err != nil {
t.Fatalf("Read: %v", err)
}
if string(data) != "content1" {
t.Errorf("data = %q, want 'content1'", data)
}
}
// ══════════════════════════════════════════════════════════════════
// Builder tests - Each bindings error
// ══════════════════════════════════════════════════════════════════
func TestEachBindingsError(t *testing.T) {
spec := DirNode[TestCtx]("/",
Each("{id}", func(ctx TestCtx) ([]Binding[TestCtx], error) {
return nil, errors.New("bindings error")
},
FileNode[TestCtx]("data", 0444),
),
)
tree := BuildTree(spec, TestCtx{})
// List should return empty (error is swallowed)
entries, err := tree.List()
if err != nil {
t.Fatalf("List: %v", err)
}
if len(entries) != 0 {
t.Errorf("entries = %d, want 0", len(entries))
}
}
// ══════════════════════════════════════════════════════════════════
// Builder tests - statDir
// ══════════════════════════════════════════════════════════════════
func TestStatDirSelf(t *testing.T) {
spec := DirNode[TestCtx]("/",
DirNode[TestCtx]("subdir",
FileNode[TestCtx]("file", 0444),
),
)
tree := BuildTree(spec, TestCtx{})
// Stat directory itself
fi, err := tree.Stat("")
if err != nil {
t.Fatalf("Stat: %v", err)
}
if !fi.IsDir() {
t.Error("should be dir")
}
// Stat with "."
fi2, err := tree.Stat(".")
if err != nil {
// This might not be supported, which is OK
return
}
if !fi2.IsDir() {
t.Error("should be dir")
}
}
func TestStatDirChild(t *testing.T) {
spec := DirNode[TestCtx]("/",
DirNode[TestCtx]("subdir",
FileNode[TestCtx]("file", 0444),
),
)
tree := BuildTree(spec, TestCtx{})
// Stat child directory
fi, err := tree.Stat("subdir")
if err != nil {
t.Fatalf("Stat: %v", err)
}
if !fi.IsDir() {
t.Error("should be dir")
}
if fi.Name() != "subdir" {
t.Errorf("Name = %q", fi.Name())
}
}
func TestStatDirFile(t *testing.T) {
spec := DirNode[TestCtx]("/",
FileNode[TestCtx]("myfile", 0644),
)
tree := BuildTree(spec, TestCtx{})
fi, err := tree.Stat("myfile")
if err != nil {
t.Fatalf("Stat: %v", err)
}
if fi.IsDir() {
t.Error("should not be dir")
}
if fi.Name() != "myfile" {
t.Errorf("Name = %q", fi.Name())
}
}
func TestStatDirWithStatOverride(t *testing.T) {
spec := DirNode[TestCtx]("/",
FileNode[TestCtx]("custom", 0444,
StatOverride[TestCtx](func() os.FileInfo {
return &SyntheticFileInfo{Name_: "override", Size_: 123}
}),
),
)
tree := BuildTree(spec, TestCtx{})
fi, err := tree.Stat("custom")
if err != nil {
t.Fatalf("Stat: %v", err)
}
if fi.Name() != "override" {
t.Errorf("Name = %q", fi.Name())
}
if fi.Size() != 123 {
t.Errorf("Size = %d", fi.Size())
}
}
func TestStatDirNotFound(t *testing.T) {
spec := DirNode[TestCtx]("/",
FileNode[TestCtx]("file", 0444),
)
tree := BuildTree(spec, TestCtx{})
_, err := tree.Stat("nonexistent")
if err == nil {
t.Error("Stat should fail for nonexistent")
}
}
// ══════════════════════════════════════════════════════════════════
// Builder tests - create not supported
// ══════════════════════════════════════════════════════════════════
func TestCreateNotSupported(t *testing.T) {
spec := DirNode[TestCtx]("/",
FileNode[TestCtx]("file", 0444),
// No create handler — always returns error
)
tree := BuildTree(spec, TestCtx{})
err := tree.Create("newfile")
if err == nil {
t.Error("Create should fail when not supported")
}
}
// ══════════════════════════════════════════════════════════════════
// Builder tests - UID/GID inheritance
// ══════════════════════════════════════════════════════════════════
func TestUIDGIDInheritance(t *testing.T) {
spec := DirNode[TestCtx]("/",
UID[TestCtx]("rootuser"),
GID[TestCtx]("rootgroup"),
DirNode[TestCtx]("sub",
// Should inherit from parent
FileNode[TestCtx]("file", 0444),
),
DirNode[TestCtx]("sub2",
UID[TestCtx]("subuser"),
FileNode[TestCtx]("file2", 0444,
UID[TestCtx]("fileuser"), // Explicitly set UID on file
),
),
)
tree := BuildTree(spec, TestCtx{})
// Check inherited values - sub/file inherits from root
f, err := tree.Open("sub/file")
if err != nil {
t.Fatalf("Open: %v", err)
}
fi, err := f.Stat()
if err != nil {
t.Fatalf("Stat: %v", err)
}
sfi := fi.(*SyntheticFileInfo)
if sfi.UID_ != "rootuser" {
t.Errorf("UID = %q, want 'rootuser'", sfi.UID_)
}
if sfi.GID_ != "rootgroup" {
t.Errorf("GID = %q, want 'rootgroup'", sfi.GID_)
}
// Check overridden UID on file
f2, err := tree.Open("sub2/file2")
if err != nil {
t.Fatalf("Open: %v", err)
}
fi2, err := f2.Stat()
if err != nil {
t.Fatalf("Stat: %v", err)
}
sfi2 := fi2.(*SyntheticFileInfo)
if sfi2.UID_ != "fileuser" {
t.Errorf("UID = %q, want 'fileuser'", sfi2.UID_)
}
// GID should still be inherited from root
if sfi2.GID_ != "rootgroup" {
t.Errorf("GID = %q, want 'rootgroup'", sfi2.GID_)
}
}
// ══════════════════════════════════════════════════════════════════
// Builder tests - mode defaults
// ══════════════════════════════════════════════════════════════════
func TestModeDefaults(t *testing.T) {
spec := DirNode[TestCtx]("/",
DirNode[TestCtx]("subdir",
FileNode[TestCtx]("file", 0644), // Explicit mode
),
)
tree := BuildTree(spec, TestCtx{})
// Check dir mode - 0 defaults to 0755
fi, err := tree.Stat("subdir")
if err != nil {
t.Fatalf("Stat: %v", err)
}
if fi.Mode()&os.ModePerm != 0755 {
t.Errorf("dir mode = %o, want 0755", fi.Mode()&os.ModePerm)
}
// Check file mode - explicit 0644
fi2, err := tree.Stat("subdir/file")
if err != nil {
t.Fatalf("Stat: %v", err)
}
if fi2.Mode() != 0644 {
t.Errorf("file mode = %o, want 0644", fi2.Mode())
}
// Also test that root dir defaults to 0755
fi3, err := tree.Stat("")
if err != nil {
t.Fatalf("Stat root: %v", err)
}
if fi3.Mode()&os.ModePerm != 0755 {
t.Errorf("root mode = %o, want 0755", fi3.Mode()&os.ModePerm)
}
}
// ══════════════════════════════════════════════════════════════════
// Original tests (preserved)
// ══════════════════════════════════════════════════════════════════
func TestEachBasic(t *testing.T) {
type testData struct {
ID string
Name string
}
instances := []testData{
{ID: "aaa-111", Name: "first"},
{ID: "bbb-222", Name: "second"},
}
spec := DirNode[TestCtx]("/",
Each("{id}", func(ctx TestCtx) ([]Binding[TestCtx], error) {
var out []Binding[TestCtx]
for _, d := range instances {
data := d // capture
out = append(out, Binding[TestCtx]{
Name: data.Name,
Aliases: []string{data.ID},
UID: data.ID,
GID: "testgroup",
})
}
return out, nil
},
FileNode[TestCtx]("id", 0444, Read(func(ctx TestCtx) ([]byte, error) {
return []byte("test-id\n"), nil
})),
FileNode[TestCtx]("name", 0666,
Read(func(ctx TestCtx) ([]byte, error) {
return []byte("test-name\n"), nil
}),
Write(func(ctx TestCtx, data []byte) error {
return nil // no-op for test
}),
),
),
)
tree := BuildTree(spec, TestCtx{})
// Test: List root should show the two instances by name
entries, err := tree.List()
if err != nil {
t.Fatalf("List root: %v", err)
}
if len(entries) != 2 {
t.Errorf("List root: got %d entries, want 2", len(entries))
}
names := make(map[string]bool)
for _, e := range entries {
names[e.Name()] = true
}
if !names["first"] {
t.Error("List root: missing 'first'")
}
if !names["second"] {
t.Error("List root: missing 'second'")
}
// Test: Access by name
sub, err := tree.Readdir("first")
if err != nil {
t.Fatalf("Readdir(first): %v", err)
}
foundID := false
foundName := false
for _, e := range sub {
switch e.Name() {
case "id":
foundID = true
case "name":
foundName = true
}
}
if !foundID {
t.Error("Readdir(first): missing 'id' leaf")
}
if !foundName {
t.Error("Readdir(first): missing 'name' leaf")
}
// Test: Access by alias
subByAlias, err := tree.Readdir("aaa-111")
if err != nil {
t.Fatalf("Readdir(aaa-111): %v", err)
}
if len(subByAlias) != len(sub) {
t.Errorf("Readdir by alias: got %d entries, want %d", len(subByAlias), len(sub))
}
// Test: Read leaf file
f, err := tree.Open("first/id")
if err != nil {
t.Fatalf("Open(first/id): %v", err)
}
data, err := f.Read()
if err != nil {
t.Fatalf("Read(first/id): %v", err)
}
if string(data) != "test-id\n" {
t.Errorf("Read(first/id) = %q, want %q", data, "test-id\n")
}
// Test: Stat instance directory
fi, err := tree.Stat("first")
if err != nil {
t.Fatalf("Stat(first): %v", err)
}
if !fi.IsDir() {
t.Error("Stat(first): expected directory")
}
if fi.Name() != "first" {
t.Errorf("Stat(first).Name() = %q, want 'first'", fi.Name())
}
// Test: Stat via alias
fiAlias, err := tree.Stat("aaa-111")
if err != nil {
t.Fatalf("Stat(aaa-111): %v", err)
}
if fiAlias.Name() != "aaa-111" {
t.Errorf("Stat(aaa-111).Name() = %q, want 'aaa-111'", fiAlias.Name())
}
}
func TestContextApplier(t *testing.T) {
type Ctx struct {
UserID string
}
spec := DirNode[Ctx]("/",
Each("{user}", func(ctx Ctx) ([]Binding[Ctx], error) {
return []Binding[Ctx]{
{
Name: "alice",
Applier: func(c Ctx) Ctx {
c.UserID = "alice-123"
return c
},
},
{
Name: "bob",
Applier: func(c Ctx) Ctx {
c.UserID = "bob-456"
return c
},
},
}, nil
},
FileNode[Ctx]("whoami", 0444, Read(func(ctx Ctx) ([]byte, error) {
return []byte(ctx.UserID + "\n"), nil
})),
),
)
tree := BuildTree(spec, Ctx{})
// Test: Read alice's whoami
f, err := tree.Open("alice/whoami")
if err != nil {
t.Fatalf("Open(alice/whoami): %v", err)
}
data, err := f.Read()
if err != nil {
t.Fatalf("Read: %v", err)
}
if string(data) != "alice-123\n" {
t.Errorf("alice/whoami = %q, want 'alice-123\\n'", data)
}
// Test: Read bob's whoami
f2, err := tree.Open("bob/whoami")
if err != nil {
t.Fatalf("Open(bob/whoami): %v", err)
}
data2, err := f2.Read()
if err != nil {
t.Fatalf("Read: %v", err)
}
if string(data2) != "bob-456\n" {
t.Errorf("bob/whoami = %q, want 'bob-456\\n'", data2)
}
}
func TestGenerateHelp(t *testing.T) {
spec := DirNode[TestCtx]("/",
FileNode[TestCtx]("version", 0444,
Doc[TestCtx]("API version"),
Read(func(ctx TestCtx) ([]byte, error) {
return []byte("1.0\n"), nil
}),
),
DirNode[TestCtx]("data",
Doc[TestCtx]("Data directory"),
FileNode[TestCtx]("config", 0666,
Doc[TestCtx]("Configuration file"),
),
),
)
help := GenerateHelp(spec)
if help == "" {
t.Error("GenerateHelp returned empty string")
}
if !contains(help, "version") {
t.Error("help missing 'version'")
}
if !contains(help, "API version") {
t.Error("help missing 'API version' description")
}
if !contains(help, "data") {
t.Error("help missing 'data'")
}
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsAt(s, substr))
}
func containsAt(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
func TestStatMode(t *testing.T) {
spec := DirNode[TestCtx]("/",
Each("{id}", func(ctx TestCtx) ([]Binding[TestCtx], error) {
return []Binding[TestCtx]{{Name: "instance"}}, nil
},
FileNode[TestCtx]("readable", 0444, Read(func(ctx TestCtx) ([]byte, error) {
return []byte("r\n"), nil
})),
FileNode[TestCtx]("writable", 0666,
Read(func(ctx TestCtx) ([]byte, error) {
return []byte("rw\n"), nil
}),
Write(func(ctx TestCtx, data []byte) error {
return nil
}),
),
FileNode[TestCtx]("executable", 0555, Read(func(ctx TestCtx) ([]byte, error) {
return []byte("#!/bin/sh\n"), nil
})),
),
)
tree := BuildTree(spec, TestCtx{})
tests := []struct {
path string
mode os.FileMode
}{
{"instance", os.ModeDir | 0755},
{"instance/readable", 0444},
{"instance/writable", 0666},
{"instance/executable", 0555},
}
for _, tc := range tests {
fi, err := tree.Stat(tc.path)
if err != nil {
t.Errorf("Stat(%q): %v", tc.path, err)
continue
}
if fi.Mode() != tc.mode {
t.Errorf("Stat(%q).Mode() = %o, want %o", tc.path, fi.Mode(), tc.mode)
}
}
}