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") } }