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//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//working/: %s", want, body) } } }