package fs import ( "context" "os" "path/filepath" "testing" "codeberg.org/VARASYS/ZDDC/zddc/internal/zddc" ) func setupTreeRoot(t *testing.T) string { t.Helper() root := t.TempDir() // Permissive root .zddc so subdirectory ACL checks pass. if err := os.WriteFile(filepath.Join(root, ".zddc"), []byte("acl:\n permissions:\n \"*@example.com\": rwcda\n"), 0o644); err != nil { t.Fatal(err) } zddc.InvalidateCache(root) return root } // Listing a cascade-declared peer that doesn't exist on disk yet returns // an empty slice instead of os.ErrNotExist, so fresh projects don't 404. // (The tables peers mdl/rsk/ssr instead surface their synthetic spec // entries; see TestListDirectory_MdlAggregate.) func TestListDirectory_DeclaredPeerEmptyWhenMissing(t *testing.T) { root := setupTreeRoot(t) if err := os.MkdirAll(filepath.Join(root, "Proj"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(root, "Proj", ".zddc"), []byte("acl:\n permissions:\n \"*@example.com\": rwcda\n"), 0o644); err != nil { t.Fatal(err) } zddc.InvalidateCache(root) for _, peer := range []string{"working", "staging", "reviewing", "incoming"} { dirPath := "Proj/" + peer got, err := ListDirectory(context.Background(), nil, root, dirPath, "alice@example.com", "/"+dirPath+"/", false, false) if err != nil { t.Errorf("ListDirectory(%s) on missing peer: err = %v, want nil", dirPath, err) continue } if len(got) != 0 { t.Errorf("ListDirectory(%s) on missing peer: got %+v, want empty", dirPath, got) } } } // Non-canonical paths still 404 (return os.ErrNotExist) — the fallback // only applies to cascade-declared paths. func TestListDirectory_NonCanonicalMissing_StillNotFound(t *testing.T) { root := setupTreeRoot(t) if err := os.MkdirAll(filepath.Join(root, "Proj"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(root, "Proj", ".zddc"), []byte("acl:\n permissions:\n \"*@example.com\": rwcda\n"), 0o644); err != nil { t.Fatal(err) } zddc.InvalidateCache(root) _, err := ListDirectory(context.Background(), nil, root, "Proj/random-folder-that-doesnt-exist", "alice@example.com", "/Proj/random-folder-that-doesnt-exist/", false, false) if !os.IsNotExist(err) { t.Errorf("ListDirectory on a non-canonical missing path: err = %v, want IsNotExist", err) } } // The mdl/ peer root lists its party subdirs (folder-nav) plus the // synthetic table.yaml/form.yaml spec entries. The tables tool builds // the cross-party aggregate by recursing into the party subdirs // client-side; the server just lists normally + advertises the spec. func TestListDirectory_MdlAggregate(t *testing.T) { root := setupTreeRoot(t) mk := func(p string) { if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(p, []byte("id: x\n"), 0o644); err != nil { t.Fatal(err) } } mk(filepath.Join(root, "Proj", "mdl", "Acme", "D-001.yaml")) mk(filepath.Join(root, "Proj", "mdl", "Beta", "D-009.yaml")) zddc.InvalidateCache(root) got, err := ListDirectory(context.Background(), nil, root, "Proj/mdl", "alice@example.com", "/Proj/mdl/", false, false) if err != nil { t.Fatalf("list: %v", err) } names := map[string]bool{} for _, fi := range got { names[fi.Name] = true } for _, want := range []string{"Acme/", "Beta/", "table.yaml", "form.yaml"} { if !names[want] { t.Errorf("mdl/ listing missing %q; got %+v", want, got) } } } // TestListDirectory_VerbsPerEntry — every entry in a directory listing // carries `verbs`, the canonical "rwcda" subset granted to the caller // at that entry's URL. Files and dirs are gated against different // chains (files use parent's, dirs use their own), so a fenced subdir // surfaces a different verb set than its file siblings. func TestListDirectory_VerbsPerEntry(t *testing.T) { root := t.TempDir() // Root grants alice read across the project; bob nothing. if err := os.WriteFile(filepath.Join(root, ".zddc"), []byte("acl:\n permissions:\n \"alice@example.com\": rw\n"), 0o644); err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Join(root, "Proj", "sub"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(root, "Proj", "doc.md"), []byte("x"), 0o644); err != nil { t.Fatal(err) } // Subdir extends alice's grant to include create — confirms the // dir entry's verbs come from its OWN chain, not parent's. if err := os.WriteFile(filepath.Join(root, "Proj", "sub", ".zddc"), []byte("acl:\n permissions:\n \"alice@example.com\": rwc\n"), 0o644); err != nil { t.Fatal(err) } zddc.InvalidateCache(root) got, err := ListDirectory(context.Background(), nil, root, "Proj", "alice@example.com", "/Proj/", false, false) if err != nil { t.Fatalf("list: %v", err) } wantVerbs := map[string]string{ "doc.md": "rw", // file: parent chain (project root → rw) "sub/": "rwc", // dir: own chain (extends to rwc) } for _, fi := range got { want, ok := wantVerbs[fi.Name] if !ok { continue } if fi.Verbs != want { t.Errorf("entry %s verbs = %q, want %q", fi.Name, fi.Verbs, want) } if !fi.IsDir { wantWritable := want == "rw" if fi.Writable != wantWritable { t.Errorf("entry %s Writable = %v, want %v", fi.Name, fi.Writable, wantWritable) } } } } // TestListDirectory_VerbsActiveAdminBypass — an elevated admin sees the // full "rwcda" verb set on every entry regardless of explicit ACL // grants. Mirrors the InternalDecider's single bypass branch. func TestListDirectory_VerbsActiveAdminBypass(t *testing.T) { root := t.TempDir() if err := os.WriteFile(filepath.Join(root, ".zddc"), []byte("admins:\n - admin@example.com\n"), 0o644); err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Join(root, "Proj"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(root, "Proj", "doc.md"), []byte("x"), 0o644); err != nil { t.Fatal(err) } zddc.InvalidateCache(root) got, err := ListDirectory(context.Background(), nil, root, "Proj", "admin@example.com", "/Proj/", false, true /* elevated */) if err != nil { t.Fatalf("list: %v", err) } for _, fi := range got { if fi.Verbs != "rwcda" { t.Errorf("elevated admin %s verbs = %q, want rwcda", fi.Name, fi.Verbs) } } got, err = ListDirectory(context.Background(), nil, root, "Proj", "admin@example.com", "/Proj/", false, false) if err != nil { t.Fatalf("list un-elevated: %v", err) } for _, fi := range got { if fi.Verbs == "rwcda" { t.Errorf("un-elevated admin %s verbs = %q, should not be full grant", fi.Name, fi.Verbs) } } }