tracker/usecase_approval_test.go
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())
}
}