ZDDC/zddc/internal/handler/accepthandler_test.go
2026-06-11 13:32:31 -05:00

195 lines
8 KiB
Go

package handler
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
"codeberg.org/VARASYS/ZDDC/zddc/internal/zddc"
)
// acceptSetup writes a tree with a conforming transmittal folder under
// archive/Acme/incoming/ and an admin grant for alice. Returns the cfg,
// a do() helper, and the root path.
func acceptSetup(t *testing.T) (config.Config, func(target, email string, body []byte) *httptest.ResponseRecorder, string) {
t.Helper()
root := t.TempDir()
mustWriteHelper(t, filepath.Join(root, ".zddc"),
"admins:\n - alice@example.com\n"+
"roles:\n document_controller:\n members: [alice@example.com]\n")
for _, d := range []string{"Project-1/incoming/Acme/2026-05-15_Acme-0042 (RFI) - Foundation"} {
if err := os.MkdirAll(filepath.Join(root, d), 0o755); err != nil {
t.Fatalf("mkdir %s: %v", d, err)
}
}
// Register the party (party_source: ssr) so filing isn't 409'd.
mustWriteHelper(t, filepath.Join(root, "Project-1/ssr/Acme.yaml"), "kind: SSR\n")
// Seed two conforming files inside the transmittal folder.
transmittalDir := filepath.Join(root, "Project-1/incoming/Acme/2026-05-15_Acme-0042 (RFI) - Foundation")
mustWriteHelper(t, filepath.Join(transmittalDir, "Acme-0042_A (RFI) - Foundation.pdf"), "%PDF-")
mustWriteHelper(t, filepath.Join(transmittalDir, "Acme-0042_A (RFI) - Cover Letter.pdf"), "%PDF-")
zddc.InvalidateCache(root)
cfg := config.Config{
Root: root,
EmailHeader: "X-Auth-Request-Email",
MaxWriteBytes: 64 * 1024,
}
do := func(target, email string, body []byte) *httptest.ResponseRecorder {
// target may contain spaces and parens (real transmittal folder
// names do); construct the URL from a url.URL so the request line
// gets properly escaped and r.URL.Path comes back decoded for the
// handler's pattern match.
u := &url.URL{Path: target}
req := httptest.NewRequest(http.MethodPost, u.RequestURI(), bytes.NewReader(body))
req.Header.Set(headerOp, opAcceptTransmittal)
req.Header.Set("Content-Type", "application/yaml")
ctx := context.WithValue(req.Context(), EmailKey, email)
ctx = context.WithValue(ctx, ElevatedKey, true)
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
ServeFileAPI(cfg, rec, req)
return rec
}
return cfg, do, root
}
// TestAccept_FreshAcceptance — a conforming transmittal folder moves
// from incoming/ to received/, renamed to tracking-only.
func TestAccept_FreshAcceptance(t *testing.T) {
_, do, root := acceptSetup(t)
target := "/Project-1/incoming/Acme/2026-05-15_Acme-0042 (RFI) - Foundation/"
rec := do(target, "alice@example.com", nil)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d, want 200; body=%s", rec.Code, rec.Body.String())
}
var resp acceptResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v; body=%s", err, rec.Body.String())
}
if resp.Tracking != "Acme-0042" {
t.Errorf("Tracking=%q, want Acme-0042", resp.Tracking)
}
if resp.MovedFiles != 2 {
t.Errorf("MovedFiles=%d, want 2", resp.MovedFiles)
}
if resp.Merged {
t.Errorf("Merged=true, want false on fresh acceptance")
}
// Folder should be at received/Acme-0042/, not the transmittal name.
if _, err := os.Stat(filepath.Join(root, "Project-1/archive/Acme/received/Acme-0042/Acme-0042_A (RFI) - Foundation.pdf")); err != nil {
t.Errorf("primary file not moved into received/: %v", err)
}
// Source should no longer exist.
if _, err := os.Stat(filepath.Join(root, "Project-1/incoming/Acme/2026-05-15_Acme-0042 (RFI) - Foundation")); !os.IsNotExist(err) {
t.Errorf("source folder still present after rename")
}
}
// TestAccept_NonConformingFilename — a file inside the transmittal
// folder that doesn't parse rejects the whole accept and leaves the
// source untouched.
func TestAccept_NonConformingFilename(t *testing.T) {
_, do, root := acceptSetup(t)
// Drop a bad file alongside the good ones.
mustWriteHelper(t, filepath.Join(root, "Project-1/incoming/Acme/2026-05-15_Acme-0042 (RFI) - Foundation/random-notes.txt"), "oops")
rec := do("/Project-1/incoming/Acme/2026-05-15_Acme-0042 (RFI) - Foundation/", "alice@example.com", nil)
if rec.Code != http.StatusConflict {
t.Fatalf("status=%d, want 409; body=%s", rec.Code, rec.Body.String())
}
if !strings.Contains(rec.Body.String(), "random-notes.txt") {
t.Errorf("error body should name the violating file; got %s", rec.Body.String())
}
// Source untouched.
if _, err := os.Stat(filepath.Join(root, "Project-1/incoming/Acme/2026-05-15_Acme-0042 (RFI) - Foundation")); err != nil {
t.Errorf("source folder removed despite rejection: %v", err)
}
}
// TestAccept_NonConformingFolderName — a transmittal folder whose
// name doesn't parse rejects with 400 (the URL pattern matches the
// outer shape but the folder grammar fails).
func TestAccept_NonConformingFolderName(t *testing.T) {
_, do, root := acceptSetup(t)
badDir := filepath.Join(root, "Project-1/incoming/Acme/bad-folder-name")
if err := os.MkdirAll(badDir, 0o755); err != nil {
t.Fatal(err)
}
rec := do("/Project-1/incoming/Acme/bad-folder-name/", "alice@example.com", nil)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status=%d, want 400; body=%s", rec.Code, rec.Body.String())
}
}
// TestAccept_PlanReviewChain — setup_plan_review: true chains into
// Plan Review and reports both results in the response.
func TestAccept_PlanReviewChain(t *testing.T) {
_, do, root := acceptSetup(t)
body := []byte(strings.Join([]string{
"setup_plan_review: true",
"review_lead: bob@vendor.com",
"approver: carol@example.com",
"plan_review_complete_date: 2026-05-30",
"plan_response_date: 2026-06-15",
"",
}, "\n"))
rec := do("/Project-1/incoming/Acme/2026-05-15_Acme-0042 (RFI) - Foundation/", "alice@example.com", body)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d, want 200; body=%s", rec.Code, rec.Body.String())
}
var resp acceptResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp.PlanReview == nil {
t.Fatalf("PlanReview chain absent in response: %+v", resp)
}
if !resp.PlanReview.Reviewing.Created || !resp.PlanReview.Staging.Created {
t.Errorf("chained Plan Review did not converge: %+v", resp.PlanReview)
}
// received/.zddc must exist (Plan Review writes it).
if _, err := os.Stat(filepath.Join(root, "Project-1/archive/Acme/received/Acme-0042/.zddc")); err != nil {
t.Errorf("received .zddc not written by chained Plan Review: %v", err)
}
}
// TestAccept_Merge — a second acceptance of the same tracking with
// distinct filenames merges into the existing received/<tracking>/
// folder. Re-using a filename is rejected by WORM.
func TestAccept_Merge(t *testing.T) {
_, do, root := acceptSetup(t)
rec := do("/Project-1/incoming/Acme/2026-05-15_Acme-0042 (RFI) - Foundation/", "alice@example.com", nil)
if rec.Code != http.StatusOK {
t.Fatalf("first accept status=%d, want 200; body=%s", rec.Code, rec.Body.String())
}
// Build a second transmittal folder with the same tracking but a
// distinct rev so the filenames don't collide.
secondDir := filepath.Join(root, "Project-1/incoming/Acme/2026-06-01_Acme-0042 (RFI) - Followup")
if err := os.MkdirAll(secondDir, 0o755); err != nil {
t.Fatal(err)
}
mustWriteHelper(t, filepath.Join(secondDir, "Acme-0042_B (RFI) - Foundation.pdf"), "%PDF-")
rec = do("/Project-1/incoming/Acme/2026-06-01_Acme-0042 (RFI) - Followup/", "alice@example.com", nil)
if rec.Code != http.StatusOK {
t.Fatalf("second accept status=%d, want 200; body=%s", rec.Code, rec.Body.String())
}
var resp acceptResponse
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
if !resp.Merged {
t.Errorf("Merged=false on re-acceptance of same tracking; want true")
}
// Both revs should now live in received/Acme-0042/.
for _, name := range []string{"Acme-0042_A (RFI) - Foundation.pdf", "Acme-0042_B (RFI) - Foundation.pdf"} {
if _, err := os.Stat(filepath.Join(root, "Project-1/archive/Acme/received/Acme-0042", name)); err != nil {
t.Errorf("expected %s in merged received/: %v", name, err)
}
}
}