ZDDC/zddc/internal/handler/zddcfile_test.go
ZDDC a0a3f8579b feat(zddcfile): virtual .zddc body = leaf cascade level as YAML
When no .zddc is on disk at the requested directory, ServeZddcFile
now renders the cascade's leaf-level ZddcFile as YAML — what
defaults.zddc.yaml's paths: tree declares for THIS exact path,
threaded through by the walker. The previous body was a comment-
only summary plus a `{}` placeholder, which forced operators to
write any override from scratch.

The .zddc file is still the single source of truth — no synthesis,
no merge: the virtual body IS the embedded subtree, marshalled in
the same shape the operator would write themselves. PUT-saving the
bytes back through the file API materialises an on-disk override
carrying exactly what the user saved. For the COMPOSED view across
the full chain, slice 2 will add ?effective=1 (returns JSON, not a
.zddc); the header comment in the virtual body points at it.

Three new test cases lock the contract:
  - VirtualDefault: at /Project/.zddc with no on-disk file, the
    embedded paths.* contribution surfaces (project_team: r,
    observer: r, archive subtree, …).
  - VirtualEmpty: at a path the embedded defaults don't declare
    (e.g. /Project/random-subfolder/.zddc), the body collapses to
    the header + an empty-document {} placeholder + an explanation
    that rules come from ancestors only.
  - VirtualPerPartyWorking: at /Project/archive/Acme/working/.zddc,
    the body carries default_tool/auto_own/drop_target and the
    classifier in available_tools — the per-party in-flight slot's
    full declaration.

Drive-by: add `omitempty` to ZddcFile.ACL, .Admins, .Title yaml
tags. Without it, the marshaled virtual body carried `acl: {}`,
`admins: []`, and `title: ""` at every nested level, drowning the
real content in noise. ParseFile is unaffected (input parsing
ignores omitempty); WriteFile's round-trip sanity check still
passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 09:32:15 -05:00

195 lines
6.9 KiB
Go

package handler
import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
"codeberg.org/VARASYS/ZDDC/zddc/internal/zddc"
)
func TestIsZddcFileRequest(t *testing.T) {
cases := []struct {
url string
want bool
}{
{"/.zddc", true},
{"/.zddc/", true},
{"/Project/.zddc", true},
{"/Project/archive/PartyA/mdl/.zddc", true},
{"/.zddc.html", false}, // editor leaf, handled separately
{"/Project/.zddc.html", false},
{"/Project/.zddc/", true},
{"/Project/", false},
{"/", false},
}
for _, tc := range cases {
if got := IsZddcFileRequest(tc.url); got != tc.want {
t.Errorf("IsZddcFileRequest(%q) = %v, want %v", tc.url, got, tc.want)
}
}
}
func TestServeZddcFile_ExistingFile(t *testing.T) {
root := t.TempDir()
mustWrite(t, filepath.Join(root, ".zddc"),
"title: root\nacl:\n permissions:\n \"*\": rwcda\n")
subDir := filepath.Join(root, "Project")
if err := os.Mkdir(subDir, 0o755); err != nil {
t.Fatal(err)
}
mustWrite(t, filepath.Join(subDir, ".zddc"),
"title: project-level\n")
zddc.InvalidateCache(root)
cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"}
req := httptest.NewRequest(http.MethodGet, "/Project/.zddc", nil)
req = req.WithContext(context.WithValue(req.Context(), EmailKey, "alice@example.com"))
rec := httptest.NewRecorder()
ServeZddcFile(cfg, rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
}
if !strings.Contains(rec.Body.String(), "title: project-level") {
t.Errorf("body missing on-disk content: %q", rec.Body.String())
}
if got := rec.Header().Get("X-ZDDC-Source"); !strings.HasPrefix(got, "file:") {
t.Errorf("X-ZDDC-Source = %q, want file:* prefix", got)
}
if got := rec.Header().Get("Content-Type"); !strings.HasPrefix(got, "application/yaml") {
t.Errorf("Content-Type = %q, want application/yaml*", got)
}
}
// TestServeZddcFile_VirtualDefault — when no .zddc is on disk at the
// requested directory, the body is the cascade's leaf-level ZddcFile
// marshalled as YAML, prefixed by a header comment explaining what
// the file is and pointing at ?effective=1 for the composed view.
//
// At /Project/.zddc with no on-disk file, the leaf is the embedded
// defaults' paths.* contribution — i.e. the project-scoped baseline
// (project_team: r, observer: r, document_controller: rw) plus the
// canonical paths: tree (archive, working, staging, reviewing, …).
// Asserts a few load-bearing markers; the full content is the
// `defaults.zddc.yaml` source-of-truth, which lives under
// zddc/internal/zddc and is parsed at every cascade walk.
func TestServeZddcFile_VirtualDefault(t *testing.T) {
root := t.TempDir()
// Directory exists but has no .zddc.
subDir := filepath.Join(root, "Project")
if err := os.Mkdir(subDir, 0o755); err != nil {
t.Fatal(err)
}
zddc.InvalidateCache(root)
cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"}
req := httptest.NewRequest(http.MethodGet, "/Project/.zddc", nil)
req = req.WithContext(context.WithValue(req.Context(), EmailKey, "alice@example.com"))
rec := httptest.NewRecorder()
ServeZddcFile(cfg, rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
}
if got := rec.Header().Get("X-ZDDC-Source"); got != "virtual:zddc" {
t.Errorf("X-ZDDC-Source = %q, want virtual:zddc", got)
}
body := rec.Body.String()
if !strings.Contains(body, "Virtual .zddc") {
t.Errorf("body missing virtual header comment: %q", body)
}
if !strings.Contains(body, "?effective=1") {
t.Errorf("body missing pointer to the composed-view query: %q", body)
}
// The embedded defaults declare project_team: r and
// observer: r at paths.*. Confirm both surface so the user
// sees the project-scoped baseline.
if !strings.Contains(body, "project_team: r") {
t.Errorf("body missing project_team grant from embedded defaults: %q", body)
}
if !strings.Contains(body, "observer: r") {
t.Errorf("body missing observer grant from embedded defaults: %q", body)
}
// The paths: subtree below should include archive (the only
// physical project-root child) and the virtual aggregators.
if !strings.Contains(body, "archive:") {
t.Errorf("body missing archive subtree: %q", body)
}
}
// TestServeZddcFile_VirtualEmpty — at a directory the embedded
// defaults' paths: tree does NOT cover, the body collapses to the
// header comment + an empty-document placeholder ({}). The user
// sees "no rules declared at this exact level".
func TestServeZddcFile_VirtualEmpty(t *testing.T) {
root := t.TempDir()
// /Project/random-subfolder/ is not declared in the embedded
// defaults' paths tree (paths.* matches the project name, but
// no child path matches "random-subfolder").
deep := filepath.Join(root, "Project", "random-subfolder")
if err := os.MkdirAll(deep, 0o755); err != nil {
t.Fatal(err)
}
zddc.InvalidateCache(root)
cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"}
req := httptest.NewRequest(http.MethodGet, "/Project/random-subfolder/.zddc", nil)
req = req.WithContext(context.WithValue(req.Context(), EmailKey, "alice@example.com"))
rec := httptest.NewRecorder()
ServeZddcFile(cfg, rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
}
body := rec.Body.String()
if !strings.Contains(body, "Virtual .zddc") {
t.Errorf("body missing virtual header: %q", body)
}
if !strings.Contains(body, "{}") {
t.Errorf("undeclared-level body should end in {}: %q", body)
}
if !strings.Contains(body, "inherited from ancestors") {
t.Errorf("undeclared-level body should explain inheritance: %q", body)
}
}
// TestServeZddcFile_VirtualPerPartyWorking — a deeper path declared
// by the embedded defaults (archive/<party>/working/) shows its own
// rich subtree: default_tool, available_tools, auto_own, etc.
func TestServeZddcFile_VirtualPerPartyWorking(t *testing.T) {
root := t.TempDir()
deep := filepath.Join(root, "Project", "archive", "Acme", "working")
if err := os.MkdirAll(deep, 0o755); err != nil {
t.Fatal(err)
}
zddc.InvalidateCache(root)
cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"}
req := httptest.NewRequest(http.MethodGet, "/Project/archive/Acme/working/.zddc", nil)
req = req.WithContext(context.WithValue(req.Context(), EmailKey, "alice@example.com"))
rec := httptest.NewRecorder()
ServeZddcFile(cfg, rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
}
body := rec.Body.String()
for _, want := range []string{
"default_tool: browse", // working/ default_tool
"auto_own: true", // working/ creator owns subdirs
"drop_target: true", // upload zone
"classifier", // available_tools includes classifier
} {
if !strings.Contains(body, want) {
t.Errorf("body missing %q at archive/<party>/working/: %s", want, body)
}
}
}