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/", false) 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 / 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/", false) 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/", false) 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/", false) 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/", false) 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/", false) 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 /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+"/", false) if err != nil { t.Errorf("ListDirectory(Proj/%s) on missing dir: err = %v, want nil", stage, err) continue } // working/ surfaces a synthetic / 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/", false) if !os.IsNotExist(err) { t.Errorf("ListDirectory on a non-canonical missing path: err = %v, want IsNotExist", err) } }