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