package main import ( "bytes" "encoding/json" "net/http/httptest" "strings" "sync" "testing" ) func TestUseCaseApprovalTracksExactContent(t *testing.T) { c, _ := loadUseCases("..") u := c.UC["W-49"] ds := &diagramStore{dir: t.TempDir()} if _, err := ds.approve(u, 0, sourceDigest(u), "", "", true); err == nil { t.Fatal("reviewer required") } status, err := ds.approve(u, 0, sourceDigest(u), "Reviewer", "Verified source and diagram", true) if err != nil || !status.Approved { t.Fatal("approval failed", err) } if !(&diagramStore{dir: ds.dir}).approvalStatus(u).Approved { t.Fatal("approval not persisted") } changed := u changed.Outcome += " changed" if ds.approvalStatus(changed).Approved { t.Fatal("source change must invalidate approval") } if _, err = ds.approve(changed, 0, sourceDigest(u), "Reviewer", "", true); err != errDiagramConflict { t.Fatal("stale source approved") } d, _ := ds.load(u.ID, u) d.Nodes[0].Title = "changed" d, err = ds.save(u.ID, d) if err != nil { t.Fatal(err) } if ds.approvalStatus(u).Approved { t.Fatal("diagram change must invalidate approval") } if _, err = ds.approve(u, 0, sourceDigest(u), "Reviewer", "", true); err != errDiagramConflict { t.Fatal("stale diagram approved") } status, err = ds.approve(u, d.Revision, sourceDigest(u), "Reviewer 2", "", true) if err != nil || !status.Approved || len(status.History) != 2 { t.Fatal("approval history lost", err) } d, err = ds.restore(u.ID, 1, 1) if err != nil || ds.approvalStatus(u).Approved { t.Fatal("restore requires approval again", err) } } func TestConcurrentDiagramSavesKeepHistory(t *testing.T) { c, _ := loadUseCases("..") ds := &diagramStore{dir: t.TempDir()} d := draftDiagram(c.UC["W-49"]) var wg sync.WaitGroup results := make(chan error, 2) for i := 0; i < 2; i++ { wg.Add(1) go func() { defer wg.Done(); _, err := ds.save("W-49", d); results <- err }() } wg.Wait() close(results) success, conflict := 0, 0 for err := range results { if err == nil { success++ } else if err == errDiagramConflict { conflict++ } else { t.Fatal(err) } } h, _ := ds.revisions("W-49") if success != 1 || conflict != 1 || len(h) != 1 { t.Fatal("concurrent saves lost history") } } func TestUseCaseApprovalHTTPGuards(t *testing.T) { old := repoRoot repoRoot = ".." defer func() { repoRoot = old }() mux := newMux(NewStore(t.TempDir()), NewHub()) c, _ := loadUseCases("..") hash := sourceDigest(c.UC["W-49"]) for _, tc := range []struct { origin, reviewer, hash string confirmed bool code int }{{"http://evil.test", "Reviewer", hash, true, 403}, {"http://example.com", "", hash, true, 400}, {"http://example.com", "Reviewer", hash, false, 400}, {"http://example.com", "Reviewer", "stale", true, 409}, {"http://example.com", "Reviewer", hash, true, 200}} { body, _ := json.Marshal(map[string]any{"revision": 0, "source_hash": tc.hash, "reviewer": tc.reviewer, "note": "Checked", "confirmed": tc.confirmed}) r := httptest.NewRequest("POST", "/api/use-cases/W-49/approval", bytes.NewReader(body)) r.Header.Set("Origin", tc.origin) w := httptest.NewRecorder() mux.ServeHTTP(w, r) if w.Code != tc.code { t.Fatalf("got %d expected %d: %s", w.Code, tc.code, w.Body.String()) } } w := httptest.NewRecorder() mux.ServeHTTP(w, httptest.NewRequest("GET", "/api/use-cases/W-49/approval", nil)) if w.Code != 200 || !strings.Contains(w.Body.String(), `"approved":true`) { t.Fatal("approval status not readable", w.Body.String()) } }