ZDDC/zddc/internal/handler/zddcfile_test.go
ZDDC 1e0e403f1e feat(zddc): retire defaults.zddc.yaml; .zddc.zip is the policy carrier (phase 6)
Completes the migration. The embedded per-depth tree (internal/zddc/defaults/)
is now the sole source of the shipped baseline; defaults.zddc.yaml is deleted.

  - EmbeddedDefaults() assembles the tree (no yaml). show-defaults now emits a
    .zddc.zip (per-depth, "*" wildcard members) via EmbeddedDefaultsZip() —
    operators redirect it to <ROOT>/.zddc.zip (or any directory) and edit/add/
    delete individual members.
  - Dropped EmbeddedDefaultsBytes; reworked the dumpable test to validate the
    emitted zip; removed the now-redundant tree-vs-yaml oracle (the Layer-2
    matrix is the ongoing behavioral guarantee, and it stays green).
  - Swept stale "defaults.zddc.yaml" comment references to the embedded tree.
  - GRAMMAR.md §1/§6 updated: .zddc.zip is a policy bundle mountable at ANY
    directory (subtree mount; inherit:false + acl.inherit:false = island); the
    shipped baseline is the embedded bundle at the root.

Net of the 6-phase migration: policy is per-depth .zddc files in a .zddc.zip
that an operator can drop at any level to override the cascade; the engine
(Assemble + the unchanged walker) enforces it. Full Go suite + matrix green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 11:35:21 -05:00

369 lines
14 KiB
Go

package handler
import (
"context"
"encoding/json"
"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
// `internal/zddc/defaults/` 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_Effective_BasicCompose — ?effective=1 returns the
// merged composed view across embedded baseline + on-disk levels.
// Body is JSON with the merged ZddcFile and per-level source list.
func TestServeZddcFile_Effective_BasicCompose(t *testing.T) {
root := t.TempDir()
mustWrite(t, filepath.Join(root, ".zddc"),
"acl:\n permissions:\n \"alice@example.com\": rwcda\n")
proj := filepath.Join(root, "Project")
if err := os.Mkdir(proj, 0o755); err != nil {
t.Fatal(err)
}
mustWrite(t, filepath.Join(proj, ".zddc"),
"title: My Project\n")
zddc.InvalidateCache(root)
cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"}
req := httptest.NewRequest(http.MethodGet, "/Project/.zddc?effective=1", 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("Content-Type"); !strings.HasPrefix(got, "application/json") {
t.Errorf("Content-Type = %q, want application/json", got)
}
if got := rec.Header().Get("X-ZDDC-Source"); got != "virtual:effective" {
t.Errorf("X-ZDDC-Source = %q, want virtual:effective", got)
}
var view effectiveZddcView
if err := json.Unmarshal(rec.Body.Bytes(), &view); err != nil {
t.Fatalf("decode: %v (body: %s)", err, rec.Body.String())
}
if view.URLPath != "/Project" {
t.Errorf("url_path = %q, want /Project", view.URLPath)
}
// Merged should carry alice's grant (from root) AND the title
// from /Project, AND the project_team grant from the embedded
// defaults' paths.* contribution.
if view.Merged.ACL.Permissions["alice@example.com"] != "rwcda" {
t.Errorf("merged.acl.permissions missing alice's grant: %+v", view.Merged.ACL.Permissions)
}
if view.Merged.ACL.Permissions["project_team"] != "r" {
t.Errorf("merged.acl.permissions missing project_team (from embedded defaults paths.*): %+v", view.Merged.ACL.Permissions)
}
if view.Merged.Title != "My Project" {
t.Errorf("merged.title = %q, want My Project (from /Project/.zddc)", view.Merged.Title)
}
// Sources should include the embedded baseline (level -1) and
// the two on-disk levels.
var levels []int
for _, s := range view.Sources {
levels = append(levels, s.Level)
}
wantLevels := map[int]bool{-1: true, 0: true, 1: true}
for _, l := range levels {
delete(wantLevels, l)
}
if len(wantLevels) > 0 {
t.Errorf("missing source levels %v in %v", wantLevels, levels)
}
// Per-level URLs are populated.
for _, s := range view.Sources {
if s.Level == -1 && s.URL != "<embedded>" {
t.Errorf("embedded source url = %q, want <embedded>", s.URL)
}
if s.Level == 0 && s.URL != "/.zddc" {
t.Errorf("root source url = %q, want /.zddc", s.URL)
}
if s.Level == 1 && s.URL != "/Project/.zddc" {
t.Errorf("project source url = %q, want /Project/.zddc", s.URL)
}
}
}
// TestServeZddcFile_Effective_InheritFence — inherit:false at a level
// drops every ancestor (including the embedded baseline) from the
// composed view. Only the fence-and-below contribute.
func TestServeZddcFile_Effective_InheritFence(t *testing.T) {
root := t.TempDir()
mustWrite(t, filepath.Join(root, ".zddc"),
"acl:\n permissions:\n \"alice@example.com\": rwcda\n")
proj := filepath.Join(root, "Closed")
if err := os.Mkdir(proj, 0o755); err != nil {
t.Fatal(err)
}
// inherit:false on Closed/.zddc — root + embedded both drop
// out of the visible chain.
// Top-level inherit:false drops EVERY ancestor including the
// embedded baseline. (ACL.inherit:false would only fence ACL
// evaluation — roles, paths-tree, and embedded baseline still
// cascade through, which is a separate test.)
mustWrite(t, filepath.Join(proj, ".zddc"),
"inherit: false\n"+
"acl:\n inherit: false\n permissions:\n \"bob@example.com\": rwcda\n")
zddc.InvalidateCache(root)
cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"}
// Bob has the only grant inside the fence; alice's root grant
// is hidden by inherit:false so she'd 404 on the read gate.
req := httptest.NewRequest(http.MethodGet, "/Closed/.zddc?effective=1", nil)
req = req.WithContext(context.WithValue(req.Context(), EmailKey, "bob@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())
}
var view effectiveZddcView
if err := json.Unmarshal(rec.Body.Bytes(), &view); err != nil {
t.Fatalf("decode: %v", err)
}
// Alice's root grant must be invisible behind the fence.
if _, ok := view.Merged.ACL.Permissions["alice@example.com"]; ok {
t.Errorf("alice's root grant should be hidden by fence; got %+v", view.Merged.ACL.Permissions)
}
// Bob's grant at Closed/ is visible.
if view.Merged.ACL.Permissions["bob@example.com"] != "rwcda" {
t.Errorf("bob's fence-level grant missing: %+v", view.Merged.ACL.Permissions)
}
// Embedded baseline (level -1) must not appear in sources — the
// fence zeroed it.
for _, s := range view.Sources {
if s.Level == -1 {
t.Errorf("embedded baseline leaked past inherit:false fence: %+v", s)
}
if s.Level == 0 {
t.Errorf("root /.zddc leaked past inherit:false fence: %+v", s)
}
}
}
// TestServeZddcFile_Effective_RoleMemberUnion — roles defined at
// multiple levels show the union of members (the runtime ACL
// evaluator uses lookupRoleMembers' union, and the composed view
// must match).
func TestServeZddcFile_Effective_RoleMemberUnion(t *testing.T) {
root := t.TempDir()
mustWrite(t, filepath.Join(root, ".zddc"),
"acl:\n permissions:\n \"alice@example.com\": r\n"+
"roles:\n document_controller:\n members:\n - root-dc@example.com\n")
proj := filepath.Join(root, "Project")
if err := os.Mkdir(proj, 0o755); err != nil {
t.Fatal(err)
}
mustWrite(t, filepath.Join(proj, ".zddc"),
"roles:\n document_controller:\n members:\n - project-dc@example.com\n")
zddc.InvalidateCache(root)
cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"}
req := httptest.NewRequest(http.MethodGet, "/Project/.zddc?effective=1", 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())
}
var view effectiveZddcView
if err := json.Unmarshal(rec.Body.Bytes(), &view); err != nil {
t.Fatalf("decode: %v", err)
}
dc, ok := view.Merged.Roles["document_controller"]
if !ok {
t.Fatalf("merged.roles missing document_controller: %+v", view.Merged.Roles)
}
wantMembers := map[string]bool{
"root-dc@example.com": true,
"project-dc@example.com": true,
}
for _, m := range dc.Members {
delete(wantMembers, m)
}
if len(wantMembers) > 0 {
t.Errorf("document_controller members missing %v; got %v", wantMembers, dc.Members)
}
}
// TestServeZddcFile_VirtualWorkingPeer — the working/ peer declared by
// the embedded defaults shows its rich config in the synthesized virtual
// .zddc: default_tool, available_tools (classifier), party_source, history.
func TestServeZddcFile_VirtualWorkingPeer(t *testing.T) {
root := t.TempDir()
cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"}
req := httptest.NewRequest(http.MethodGet, "/Project/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
"party_source: ssr", // party gating
"classifier", // available_tools includes classifier
} {
if !strings.Contains(body, want) {
t.Errorf("body missing %q at working/ peer: %s", want, body)
}
}
}