ความคืบหน้างาน ไฟล์ต้นฉบับ

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