package handler import ( "bytes" "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" ) // ssrTestSetup builds a fresh project root with permissive top-level // ACL that lets *@example.com create + write anywhere under archive/. // Returns (cfg, do) where do dispatches a request through the same // recognize→serve path the production catch-all uses. func ssrTestSetup(t *testing.T) (config.Config, func(method, target, email, body string, headers map[string]string) *httptest.ResponseRecorder) { t.Helper() root := t.TempDir() // Project root: grant the test cohort rwc at the project level so // they can create archive// folders. if err := os.WriteFile(filepath.Join(root, ".zddc"), []byte("acl:\n permissions:\n \"*@example.com\": rwcd\n"), 0o644); err != nil { t.Fatal(err) } zddc.InvalidateCache(root) cfg := config.Config{Root: root, EmailHeader: "X-Auth-Request-Email"} do := func(method, target, email, body string, headers map[string]string) *httptest.ResponseRecorder { var req *http.Request if body != "" { req = httptest.NewRequest(method, target, bytes.NewReader([]byte(body))) req.Header.Set("Content-Type", "application/json") } else { req = httptest.NewRequest(method, target, nil) } for k, v := range headers { req.Header.Set(k, v) } ctx := context.WithValue(req.Context(), EmailKey, email) ctx = context.WithValue(ctx, ElevatedKey, true) req = req.WithContext(ctx) rec := httptest.NewRecorder() // SSR create flows through RecognizeFormRequest → ServeForm → // create-via-ssr case. Rename flows through ServeFileAPI's POST // dispatch (ssr-rename op). if method == http.MethodPost && strings.Contains(target, "/ssr/") && strings.HasSuffix(target, ".yaml") { ServeFileAPI(cfg, rec, req) return rec } formReq := RecognizeFormRequest(cfg.Root, method, target) if formReq != nil { ServeForm(cfg, formReq, rec, req) return rec } rec.WriteHeader(http.StatusNotFound) return rec } return cfg, do } func TestSSRCreate_HappyPath(t *testing.T) { cfg, do := ssrTestSetup(t) body := `{"name":"0330C1","vendorType":"subcontractor","contractNo":"PO-001","scopeSummary":"Concrete works"}` rec := do(http.MethodPost, "/Project/ssr/form.html", "casey@example.com", body, nil) if rec.Code != http.StatusCreated { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if loc := rec.Result().Header.Get("Location"); loc != "/Project/ssr/0330C1.yaml" { t.Errorf("Location=%q want /Project/ssr/0330C1.yaml", loc) } // archive/0330C1/ exists. partyDir := filepath.Join(cfg.Root, "Project", "archive", "0330C1") if info, err := os.Stat(partyDir); err != nil || !info.IsDir() { t.Fatalf("party folder not created: err=%v", err) } // .zddc auto-own grant. zf, err := os.ReadFile(filepath.Join(partyDir, ".zddc")) if err != nil { t.Fatalf("read auto-own .zddc: %v", err) } if !strings.Contains(string(zf), "casey@example.com") { t.Errorf("auto-own .zddc missing creator email; got %s", string(zf)) } // ssr.yaml exists and contains the submitted fields but NOT `name`. yamlBytes, err := os.ReadFile(filepath.Join(partyDir, "ssr.yaml")) if err != nil { t.Fatalf("read ssr.yaml: %v", err) } yaml := string(yamlBytes) if !strings.Contains(yaml, "contractNo: PO-001") { t.Errorf("ssr.yaml missing contractNo; got %s", yaml) } if strings.Contains(yaml, "name: 0330C1") { t.Errorf("ssr.yaml should not carry path-derived `name` field; got %s", yaml) } } func TestSSRCreate_AnonymousRejected(t *testing.T) { _, do := ssrTestSetup(t) body := `{"name":"0330C1","vendorType":"subcontractor","contractNo":"PO-001","scopeSummary":"x"}` rec := do(http.MethodPost, "/Project/ssr/form.html", "", body, nil) if rec.Code != http.StatusUnauthorized { t.Errorf("status=%d want 401; body=%s", rec.Code, rec.Body.String()) } } func TestSSRCreate_InvalidName(t *testing.T) { _, do := ssrTestSetup(t) cases := []string{ `{"name":".hidden","vendorType":"subcontractor","contractNo":"x","scopeSummary":"x"}`, `{"name":"with space","vendorType":"subcontractor","contractNo":"x","scopeSummary":"x"}`, } for _, body := range cases { rec := do(http.MethodPost, "/Project/ssr/form.html", "casey@example.com", body, nil) if rec.Code != http.StatusUnprocessableEntity && rec.Code != http.StatusBadRequest { t.Errorf("body=%s status=%d want 422 or 400", body, rec.Code) } } } func TestSSRCreate_DuplicateName(t *testing.T) { cfg, do := ssrTestSetup(t) body := `{"name":"0330C1","vendorType":"subcontractor","contractNo":"PO-001","scopeSummary":"x"}` rec := do(http.MethodPost, "/Project/ssr/form.html", "casey@example.com", body, nil) if rec.Code != http.StatusCreated { t.Fatalf("first create failed: status=%d body=%s", rec.Code, rec.Body.String()) } zddc.InvalidateCache(filepath.Join(cfg.Root, "Project", "archive", "0330C1")) rec = do(http.MethodPost, "/Project/ssr/form.html", "casey@example.com", body, nil) if rec.Code != http.StatusConflict { t.Errorf("duplicate create: status=%d want 409", rec.Code) } } func TestSSRRename_HappyPath(t *testing.T) { cfg, do := ssrTestSetup(t) body := `{"name":"0330C1","vendorType":"subcontractor","contractNo":"PO-001","scopeSummary":"x"}` if rec := do(http.MethodPost, "/Project/ssr/form.html", "casey@example.com", body, nil); rec.Code != http.StatusCreated { t.Fatalf("setup create failed: %d %s", rec.Code, rec.Body.String()) } // Drop an MDL row inside the party folder; it should survive the rename. mdlDir := filepath.Join(cfg.Root, "Project", "archive", "0330C1", "mdl") if err := os.MkdirAll(mdlDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(mdlDir, "D-001.yaml"), []byte("id: D-001\n"), 0o644); err != nil { t.Fatal(err) } zddc.InvalidateCache(filepath.Join(cfg.Root, "Project", "archive", "0330C1")) rec := do(http.MethodPost, "/Project/ssr/0330C1.yaml", "casey@example.com", "", map[string]string{ "X-ZDDC-Op": opSSRRename, "X-ZDDC-Destination": "/Project/ssr/0330C2.yaml", }) if rec.Code != http.StatusOK { t.Fatalf("rename failed: %d body=%s", rec.Code, rec.Body.String()) } if _, err := os.Stat(filepath.Join(cfg.Root, "Project", "archive", "0330C1")); !os.IsNotExist(err) { t.Error("source party folder still exists after rename") } if _, err := os.Stat(filepath.Join(cfg.Root, "Project", "archive", "0330C2")); err != nil { t.Errorf("destination party folder not created: %v", err) } // MDL row followed the directory rename. if _, err := os.Stat(filepath.Join(cfg.Root, "Project", "archive", "0330C2", "mdl", "D-001.yaml")); err != nil { t.Errorf("MDL row did not survive rename: %v", err) } } func TestSSRRename_CrossProjectRejected(t *testing.T) { cfg, do := ssrTestSetup(t) body := `{"name":"0330C1","vendorType":"subcontractor","contractNo":"PO-001","scopeSummary":"x"}` if rec := do(http.MethodPost, "/Project/ssr/form.html", "casey@example.com", body, nil); rec.Code != http.StatusCreated { t.Fatalf("setup create failed: %d", rec.Code) } zddc.InvalidateCache(filepath.Join(cfg.Root, "Project")) rec := do(http.MethodPost, "/Project/ssr/0330C1.yaml", "casey@example.com", "", map[string]string{ "X-ZDDC-Op": opSSRRename, "X-ZDDC-Destination": "/OtherProject/ssr/0330C1.yaml", }) if rec.Code != http.StatusBadRequest { t.Errorf("cross-project rename: status=%d want 400", rec.Code) } } func TestSSRRename_DestinationExists(t *testing.T) { cfg, do := ssrTestSetup(t) bodyA := `{"name":"0330C1","vendorType":"subcontractor","contractNo":"PO-001","scopeSummary":"x"}` bodyB := `{"name":"0330C2","vendorType":"subcontractor","contractNo":"PO-002","scopeSummary":"y"}` for _, b := range []string{bodyA, bodyB} { if rec := do(http.MethodPost, "/Project/ssr/form.html", "casey@example.com", b, nil); rec.Code != http.StatusCreated { t.Fatalf("setup create failed: %d %s", rec.Code, rec.Body.String()) } } zddc.InvalidateCache(filepath.Join(cfg.Root, "Project")) rec := do(http.MethodPost, "/Project/ssr/0330C1.yaml", "casey@example.com", "", map[string]string{ "X-ZDDC-Op": opSSRRename, "X-ZDDC-Destination": "/Project/ssr/0330C2.yaml", }) if rec.Code != http.StatusConflict { t.Errorf("rename to existing: status=%d want 409", rec.Code) } }