tracker/usecases_test.go
package main
import (
"bytes"
"encoding/json"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
func TestUseCaseCatalogAndDraft(t *testing.T) {
c, err := loadUseCases("..")
if err != nil {
t.Fatal(err)
}
if len(c.UC) != 162 || len(c.Modules) != 35 {
t.Fatalf("unexpected catalog: %d/%d", len(c.UC), len(c.Modules))
}
u, ok := c.UC["W-49"]
if !ok || u.Name == "" || len(u.Flow) == 0 {
t.Fatal("source details missing")
}
d := draftDiagram(u)
for i, n := range d.Nodes {
if i > 0 && n.X <= d.Nodes[i-1].X {
t.Fatal("draft must flow left to right")
}
}
if len(d.Nodes) != len(u.Flow)+2 || len(d.Edges) != len(d.Nodes)-1 {
t.Fatal("draft must preserve all ordered steps")
}
if err := validateDiagram(d); err != nil {
t.Fatal(err)
}
}
func TestDiagramPersistenceAndValidation(t *testing.T) {
dir := t.TempDir()
ds := &diagramStore{dir: dir}
c, err := loadUseCases("..")
if err != nil {
t.Fatal(err)
}
d := draftDiagram(c.UC["W-49"])
saved, err := ds.save("W-49", d)
if err != nil {
t.Fatal(err)
}
if saved.Revision != 1 {
t.Fatal("revision not advanced")
}
if _, err = ds.save("W-49", d); err != errDiagramConflict {
t.Fatalf("stale save: %v", err)
}
got, err := ds.load("W-49", c.UC["W-49"])
if err != nil || got.Revision != 1 {
t.Fatal("saved diagram not reloaded", err)
}
for _, mutate := range []func(*Diagram){
func(d *Diagram) { d.Nodes[0].Type = "script" },
func(d *Diagram) { d.Nodes[0].ID = d.Nodes[1].ID },
func(d *Diagram) { d.Edges[0].Target = "missing" },
func(d *Diagram) { d.Edges[0].Head = "bad" },
func(d *Diagram) { d.Nodes[0].X = -1 },
} {
bad := draftDiagram(c.UC["W-49"])
mutate(&bad)
if validateDiagram(bad) == nil {
t.Fatal("invalid graph accepted")
}
}
if _, err := os.Stat(filepath.Join(dir, "diagrams", "W-49.json")); err != nil {
t.Fatal(err)
}
}
func TestUseCaseHTTP(t *testing.T) {
old := repoRoot
repoRoot = ".."
defer func() { repoRoot = old }()
mux := newMux(NewStore(t.TempDir()), NewHub())
for _, path := range []string{"/use-cases", "/use-cases/W-49", "/api/use-cases/W-49/diagram"} {
w := httptest.NewRecorder()
mux.ServeHTTP(w, httptest.NewRequest("GET", path, nil))
if w.Code != 200 {
t.Fatalf("%s: %d %s", path, w.Code, w.Body.String())
}
}
for _, path := range []string{"/use-cases/nope", "/api/use-cases/nope/diagram"} {
w := httptest.NewRecorder()
mux.ServeHTTP(w, httptest.NewRequest("GET", path, nil))
if w.Code != 404 {
t.Fatal(path, w.Code)
}
}
c, _ := loadUseCases("..")
body, _ := json.Marshal(draftDiagram(c.UC["W-49"]))
for _, tc := range []struct {
origin string
code int
}{{"http://evil.test", 403}, {"http://example.com", 200}, {"http://example.com", 409}} {
req := httptest.NewRequest("PUT", "/api/use-cases/W-49/diagram", bytes.NewReader(body))
req.Header.Set("Origin", tc.origin)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != tc.code {
t.Fatalf("save expected %d got %d: %s", tc.code, w.Code, w.Body.String())
}
}
}
func TestUseCaseTableAndFilters(t *testing.T) {
old := repoRoot
repoRoot = ".."
defer func() { repoRoot = old }()
mux := newMux(NewStore(t.TempDir()), NewHub())
for _, tc := range []struct {
path string
rows int
}{{"/use-cases", 162}, {"/use-cases?module=REG&q=W-49", 1}, {"/use-cases?module=NOTI", 0}, {"/use-cases?q=missing-use-case-no-match", 0}} {
w := httptest.NewRecorder()
mux.ServeHTTP(w, httptest.NewRequest("GET", tc.path, nil))
body := w.Body.String()
if w.Code != 200 || !strings.Contains(body, `class="uc-table"`) || strings.Count(body, `class="uc-row"`) != tc.rows {
t.Fatalf("table %s: %d rows, status %d", tc.path, strings.Count(body, `class="uc-row"`), w.Code)
}
}
}
func TestDiagramRejectsMalformedRequests(t *testing.T) {
old := repoRoot
repoRoot = ".."
defer func() { repoRoot = old }()
dir := t.TempDir()
mux := newMux(NewStore(dir), NewHub())
c, _ := loadUseCases("..")
data, _ := json.Marshal(draftDiagram(c.UC["W-49"]))
for _, body := range []string{`{}`, `{"nodes":null,"edges":null}`, string(data) + ` {}`, `{"revision":0,"nodes":[],"edges":[],"extra":true}`, strings.Repeat("x", (2<<20)+1)} {
req := httptest.NewRequest("PUT", "/api/use-cases/W-49/diagram", strings.NewReader(body))
req.Header.Set("Origin", "http://example.com")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != 400 {
t.Fatalf("malformed request accepted: %d", w.Code)
}
}
w := httptest.NewRecorder()
mux.ServeHTTP(w, httptest.NewRequest("DELETE", "/api/use-cases/W-49/diagram", nil))
if w.Code != 405 {
t.Fatal("unsupported method accepted")
}
if _, err := os.Stat(filepath.Join(dir, "diagrams", "W-49.json")); !os.IsNotExist(err) {
t.Fatal("malformed input wrote diagram")
}
}
func TestDiagramAllowsCyclesAndEmptyCanvas(t *testing.T) {
c, _ := loadUseCases("..")
d := draftDiagram(c.UC["W-49"])
d.Edges = append(d.Edges, DiagramEdge{ID: "back", Source: "end", Target: "start", Head: "open", Tail: "diamond"})
if err := validateDiagram(d); err != nil {
t.Fatal("workflows may contain cycles", err)
}
if err := validateDiagram(Diagram{Nodes: []DiagramNode{}, Edges: []DiagramEdge{}}); err != nil {
t.Fatal("user may delete every shape", err)
}
}
func TestUseCaseChannelTabs(t *testing.T) {
c, err := loadUseCases("..")
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
channel string
id string
}{{"frontend", "W-49"}, {"backoffice", "W-49"}, {"integration", "W-36"}} {
v := ucView(c, httptest.NewRequest("GET", "/use-cases?channel="+tc.channel, nil))
found := false
for _, u := range v.Items {
if u.ID == tc.id {
found = true
}
}
if !found {
t.Fatalf("%s missing in %s", tc.id, tc.channel)
}
if len(v.Tabs) != 4 {
t.Fatal("four channel tabs required")
}
}
v := ucView(c, httptest.NewRequest("GET", "/use-cases?channel=frontend&module=REG&q=W-49", nil))
if len(v.Items) != 1 {
t.Fatal("tabs must combine with filters")
}
integration := classifyUseCase(UseCase{Actors: []string{"นายทะเบียน"}, Flow: [][2]string{{"ระบบ", "ตรวจข้อมูลและบันทึกลงฐานข้อมูล"}}})
if containsUC(integration.Channels, "integration") {
t.Fatal("generic system steps are not cross-system integration")
}
}