Listing <project>/{archive,working,staging,reviewing}/ when the folder
doesn't exist on disk now returns an empty 200 listing instead of 404.
The stage-strip nav links into these folders unconditionally; without
this fallback, clicking "Working" against a fresh project (where
working/ hasn't been written to yet) lands on a 404 page rather than
a usable empty view.
Mechanism stays consistent with the existing lazy-folder design:
- GET on missing canonical folder → 200 + empty listing (this commit)
- first WRITE under the same path → EnsureCanonicalAncestors
materialises the on-disk folder + auto-own .zddc
reviewing/ stays virtual-only (in VirtualOnlyCanonicalNames); the
fallback just makes its empty listing always renderable. The future
reviewing/ aggregator (recorded in project memory) will replace the
empty listing with the join-computed virtual entries.
The fallback is gated on IsProjectRootFolder — only depth-2 paths
matching one of the four canonical names. Non-canonical missing paths
still 404 (TestListDirectory_NonCanonicalMissing_StillNotFound).
For working/ specifically the synthetic <viewer-email>/ home entry
still fires from virtualUserHomeEntry, so the user sees their own
placeholder even when working/ doesn't exist yet — first write into
that placeholder triggers the lazy-create chain.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
206 lines
6.2 KiB
Go
206 lines
6.2 KiB
Go
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
|
|
}
|
|
|
|
func TestListDirectory_VirtualUserHome_AppearsWhenMissing(t *testing.T) {
|
|
root := setupTreeRoot(t)
|
|
if err := os.MkdirAll(filepath.Join(root, "Proj", "working"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
zddc.InvalidateCache(root)
|
|
|
|
got, err := ListDirectory(context.Background(), nil, root, "Proj/working", "alice@example.com", "/Proj/working/")
|
|
if err != nil {
|
|
t.Fatalf("list: %v", err)
|
|
}
|
|
|
|
var virtual *string
|
|
for i := range got {
|
|
if got[i].Virtual {
|
|
n := got[i].Name
|
|
virtual = &n
|
|
}
|
|
}
|
|
if virtual == nil {
|
|
t.Fatalf("expected synthetic <viewer-email>/ entry, got entries: %+v", got)
|
|
}
|
|
if *virtual != "alice@example.com/" {
|
|
t.Errorf("synthetic name = %q, want alice@example.com/", *virtual)
|
|
}
|
|
}
|
|
|
|
func TestListDirectory_VirtualUserHome_SuppressedWhenRealExists(t *testing.T) {
|
|
root := setupTreeRoot(t)
|
|
// A real folder exists for the viewer (any case).
|
|
if err := os.MkdirAll(filepath.Join(root, "Proj", "working", "Alice@Example.com"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
zddc.InvalidateCache(root)
|
|
|
|
got, err := ListDirectory(context.Background(), nil, root, "Proj/working", "alice@example.com", "/Proj/working/")
|
|
if err != nil {
|
|
t.Fatalf("list: %v", err)
|
|
}
|
|
for _, fi := range got {
|
|
if fi.Virtual {
|
|
t.Errorf("synthetic entry should be suppressed when a case-fold match exists; got %+v", fi)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListDirectory_VirtualUserHome_AnonymousNoEntry(t *testing.T) {
|
|
root := setupTreeRoot(t)
|
|
if err := os.MkdirAll(filepath.Join(root, "Proj", "working"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
zddc.InvalidateCache(root)
|
|
|
|
got, err := ListDirectory(context.Background(), nil, root, "Proj/working", "" /* no viewer */, "/Proj/working/")
|
|
if err != nil {
|
|
t.Fatalf("list: %v", err)
|
|
}
|
|
for _, fi := range got {
|
|
if fi.Virtual {
|
|
t.Errorf("anonymous viewer should not see synthetic entries; got %+v", fi)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListDirectory_VirtualUserHome_OutsideWorkingNoEntry(t *testing.T) {
|
|
root := setupTreeRoot(t)
|
|
if err := os.MkdirAll(filepath.Join(root, "Proj", "staging"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
zddc.InvalidateCache(root)
|
|
|
|
got, err := ListDirectory(context.Background(), nil, root, "Proj/staging", "alice@example.com", "/Proj/staging/")
|
|
if err != nil {
|
|
t.Fatalf("list: %v", err)
|
|
}
|
|
for _, fi := range got {
|
|
if fi.Virtual {
|
|
t.Errorf("staging/ should not have a synthetic user-home entry; got %+v", fi)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListDirectory_VirtualUserHome_DeepWorkingNoEntry(t *testing.T) {
|
|
root := setupTreeRoot(t)
|
|
// Listing inside working/ at depth 3+ — no synthetic entry should fire.
|
|
if err := os.MkdirAll(filepath.Join(root, "Proj", "working", "alice@example.com"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
zddc.InvalidateCache(root)
|
|
|
|
got, err := ListDirectory(context.Background(), nil, root,
|
|
"Proj/working/alice@example.com", "alice@example.com",
|
|
"/Proj/working/alice@example.com/")
|
|
if err != nil {
|
|
t.Fatalf("list: %v", err)
|
|
}
|
|
for _, fi := range got {
|
|
if fi.Virtual {
|
|
t.Errorf("nested working/ subdir must not synthesise the user home; got %+v", fi)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListDirectory_VirtualUserHome_CaseFoldWorking(t *testing.T) {
|
|
root := setupTreeRoot(t)
|
|
// Pre-existing PascalCase Working/.
|
|
if err := os.MkdirAll(filepath.Join(root, "Proj", "Working"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
zddc.InvalidateCache(root)
|
|
|
|
got, err := ListDirectory(context.Background(), nil, root, "Proj/Working", "alice@example.com", "/Proj/Working/")
|
|
if err != nil {
|
|
t.Fatalf("list: %v", err)
|
|
}
|
|
var found bool
|
|
for _, fi := range got {
|
|
if fi.Virtual {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("PascalCase Working/ should still surface the synthetic entry; got entries: %+v", got)
|
|
}
|
|
}
|
|
|
|
// Listing a canonical project folder that doesn't exist on disk yet
|
|
// returns an empty slice instead of os.ErrNotExist. The stage-strip
|
|
// nav links into <project>/working/ etc. unconditionally; this keeps
|
|
// fresh projects (no working/ on disk yet) from 404'ing.
|
|
func TestListDirectory_CanonicalProjectFolder_EmptyWhenMissing(t *testing.T) {
|
|
root := setupTreeRoot(t)
|
|
// Proj exists but Proj/working/ does NOT.
|
|
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 _, stage := range []string{"working", "staging", "reviewing", "archive"} {
|
|
got, err := ListDirectory(context.Background(), nil, root,
|
|
"Proj/"+stage, "alice@example.com", "/Proj/"+stage+"/")
|
|
if err != nil {
|
|
t.Errorf("ListDirectory(Proj/%s) on missing dir: err = %v, want nil", stage, err)
|
|
continue
|
|
}
|
|
// working/ surfaces a synthetic <viewer-email>/ entry; the others
|
|
// should be a flat empty listing.
|
|
if stage == "working" {
|
|
if len(got) != 1 || !got[0].Virtual {
|
|
t.Errorf("ListDirectory(Proj/working) on missing dir: got %+v, want only the virtual home entry", got)
|
|
}
|
|
} else {
|
|
if len(got) != 0 {
|
|
t.Errorf("ListDirectory(Proj/%s) on missing dir: got %+v, want empty", stage, got)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Non-canonical paths still 404 (return os.ErrNotExist) — the fallback
|
|
// only applies to the four canonical project-root folders.
|
|
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/")
|
|
if !os.IsNotExist(err) {
|
|
t.Errorf("ListDirectory on a non-canonical missing path: err = %v, want IsNotExist", err)
|
|
}
|
|
}
|