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// // 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) } } }