ollie/fsedsl/decl_test.go

296 lines
6.5 KiB
Go

package fsedsl
import (
"os"
"testing"
)
// TestCtx is a simple test context.
type TestCtx struct {
Data any
}
// TestEachBasic tests the basic Blueprint pattern:
// - Declares structure statically
// - Bindings function provides identity (name, aliases, UID/GID)
// - Handlers receive context with bound data
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())
}
}
// TestContextApplier tests that Binding.Applier correctly propagates context
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)
}
}
// TestGenerateHelp tests help generation
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
}
// TestStatMode tests file modes are preserved
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)
}
}
}