ZDDC/zddc/internal/listing/listing_test.go
ZDDC 72c0552750 feat(browse): "Show hidden" toggle — list .-prefixed and _-prefixed entries
Adds a UI checkbox next to the existing Sort dropdown that surfaces
hidden entries when ACL would otherwise allow read. Default off
(matches today's filtered behavior). On toggle, browse re-fetches
the current directory with ?hidden=1 and re-renders.

  ┌─ browse toolbar ─────────────────────────────────────────────┐
  │  Sort: [Name (A→Z) ▾]    ☐ Show hidden                       │
  └──────────────────────────────────────────────────────────────┘

Server-side surface:

  - internal/fs/tree.go ListDirectory gains an `includeHidden bool`
    parameter. The .-prefix filter (previously hard-coded) now also
    drops _-prefix entries (matches dispatch's reserved-prefix guard)
    and honors the new flag.
  - internal/handler/directory.go reads `?hidden=1` from the request
    and threads it through.
  - cmd/zddc-server/main.go dispatcher relaxes its dot-prefix and
    _-prefix guards for GET/HEAD when `?hidden=1` is set, so clicking
    a hidden entry's link works. `_app/` (apps cache) stays
    unconditionally reserved — those bytes must go through the apps
    resolver. Writes to hidden paths stay blocked (the file API has
    its own segment check that the flag does NOT relax).
  - internal/listing/listing.go: signature parity (the lower-level
    helper that's used by tests + non-cascade listing paths).

Security model unchanged: the ACL chain on the parent dir is the only
real gate. Whoever can read the dir can see its contents — toggling
"Show hidden" just stops the client-side filter from masking
.-prefixed and _-prefixed entries. Hidden paths today:

  • <dir>/.zddc                ACL YAML — already exposed via /.profile/zddc
  • <dir>/.converted/<base>    cached MD→DOCX/HTML/PDF, same sensitivity as source
  • <root>/.zddc.d/tokens/     per-token metadata; filename = sha256(token)
                               so not bearer-usable. Default root ACL
                               restricts to admins; matches /.tokens UI.
  • <root>/.zddc.d/logs/       access logs; same admins-only audience
  • <root>/_app/               cached upstream tool HTML (public)
  • <root>/_template/          install.zip scaffolding (public)

None of these contain bearer credentials or secret material that the
existing ACL doesn't already gate. The walls are still the cascade.
2026-05-13 14:45:41 -05:00

87 lines
2.3 KiB
Go

package listing
import (
"os"
"path/filepath"
"testing"
)
// TestFromDirEntriesFiltersHidden asserts that both '.' and '_' prefixed
// entries are excluded from listings — the '.' branch is the long-standing
// rule (matches dispatch dot-prefix guard); '_' is for operator-managed
// scaffolding like install.zip's _template/ that should be reachable by
// direct URL but invisible to browse.
func TestFromDirEntriesFiltersHidden(t *testing.T) {
dir := t.TempDir()
for _, name := range []string{
"Project-A",
"Project-B",
".zddc", // hidden file
".devshell", // hidden dir
"_template", // scaffolding dir
"_archive", // scaffolding dir
"_notes.txt", // scaffolding file
"normal.txt",
} {
path := filepath.Join(dir, name)
if err := os.WriteFile(path, []byte("x"), 0o644); err != nil {
t.Fatalf("write %s: %v", name, err)
}
}
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("ReadDir: %v", err)
}
got, err := FromDirEntries(entries, "/", false)
if err != nil {
t.Fatalf("FromDirEntries: %v", err)
}
want := map[string]bool{
"Project-A": true,
"Project-B": true,
"normal.txt": true,
}
if len(got) != len(want) {
var names []string
for _, e := range got {
names = append(names, e.Name)
}
t.Fatalf("got %d entries (%v), want %d (%v)", len(got), names, len(want), want)
}
for _, e := range got {
if !want[e.Name] {
t.Errorf("unexpected entry in listing: %q", e.Name)
}
}
}
// TestFromDirEntriesIncludeHidden verifies the includeHidden=true path
// surfaces dot- and underscore-prefixed entries.
func TestFromDirEntriesIncludeHidden(t *testing.T) {
dir := t.TempDir()
for _, name := range []string{".zddc", "_template", "normal.txt"} {
if err := os.WriteFile(filepath.Join(dir, name), []byte("x"), 0o644); err != nil {
t.Fatalf("write %s: %v", name, err)
}
}
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("ReadDir: %v", err)
}
got, err := FromDirEntries(entries, "/", true)
if err != nil {
t.Fatalf("FromDirEntries: %v", err)
}
want := map[string]bool{".zddc": true, "_template": true, "normal.txt": true}
if len(got) != len(want) {
var names []string
for _, e := range got {
names = append(names, e.Name)
}
t.Fatalf("got %d entries (%v), want %d", len(got), names, len(want))
}
}