194 lines
7.5 KiB
Go
194 lines
7.5 KiB
Go
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/<party>/ 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", "sam@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)
|
|
}
|
|
// Registration writes the registry row at ssr/<party>.yaml and does
|
|
// NOT create an archive party folder (that appears on first filing).
|
|
rowAbs := filepath.Join(cfg.Root, "Project", "ssr", "0330C1.yaml")
|
|
yamlBytes, err := os.ReadFile(rowAbs)
|
|
if err != nil {
|
|
t.Fatalf("read ssr/0330C1.yaml: %v", err)
|
|
}
|
|
yaml := string(yamlBytes)
|
|
if !strings.Contains(yaml, "contractNo: PO-001") {
|
|
t.Errorf("ssr row missing contractNo; got %s", yaml)
|
|
}
|
|
if strings.Contains(yaml, "name: 0330C1") {
|
|
t.Errorf("ssr row should not carry path-derived `name` field; got %s", yaml)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(cfg.Root, "Project", "archive", "0330C1")); !os.IsNotExist(err) {
|
|
t.Errorf("registration must not create archive/<party>/; got err=%v", err)
|
|
}
|
|
}
|
|
|
|
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", "sam@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", "sam@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", "sam@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", "sam@example.com", body, nil); rec.Code != http.StatusCreated {
|
|
t.Fatalf("setup create failed: %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
rec := do(http.MethodPost, "/Project/ssr/0330C1.yaml", "sam@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())
|
|
}
|
|
// Registry-only rename: the row moves to the new name; folders under
|
|
// the other peers are intentionally left untouched.
|
|
if _, err := os.Stat(filepath.Join(cfg.Root, "Project", "ssr", "0330C1.yaml")); !os.IsNotExist(err) {
|
|
t.Error("source registry row still exists after rename")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(cfg.Root, "Project", "ssr", "0330C2.yaml")); err != nil {
|
|
t.Errorf("destination registry row not created: %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", "sam@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", "sam@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", "sam@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", "sam@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)
|
|
}
|
|
}
|