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>
195 lines
6.9 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|
|
|