package main import ( "encoding/json" "net/http/httptest" "os" "path/filepath" "strings" "sync" "testing" ) func TestMockupArtifactsAndCanonicalFlows(t *testing.T) { c, err := loadMockCatalog("..") if err != nil { t.Fatal(err) } if len(c.Screens) != 170 { t.Fatal(len(c.Screens)) } uc, err := loadUseCases("..") if err != nil { t.Fatal(err) } for id, u := range uc.UC { g, err := draftMockFlow("..", u, c) if err != nil { t.Fatal(id, err) } if err = validateMockGraph(g, c); err != nil { t.Fatal(id, err) } } g, err := draftMockFlow("..", uc.UC["W-41"], c) if err != nil { t.Fatal(err) } found := false for _, e := range g.Edges { if e.Label == "ไม่จำเป็น — สมัครด้วยตนเอง" { found = true } } if !found { t.Fatal("guardian bypass lost") } if _, err = draftMockSitemap("..", c); err != nil { t.Fatal(err) } } func TestMockupSaveConflictValidationAndRestore(t *testing.T) { c, _ := loadMockCatalog("..") g, err := draftMockSitemap("..", c) if err != nil { t.Fatal(err) } s := &mockStore{dir: t.TempDir()} saved, err := s.save("sitemap", g, c) if err != nil || saved.Revision != 1 { t.Fatal(err) } if _, err = s.save("sitemap", g, c); err != errMockConflict { t.Fatal("stale save accepted", err) } saved.Nodes[0].Title = "edited" saved, err = s.save("sitemap", saved, c) if err != nil { t.Fatal(err) } restored, err := s.restore("sitemap", 2, 1, c) if err != nil || restored.Revision != 3 || restored.Nodes[0].Title == "edited" { t.Fatal("restore failed", err) } history, err := s.history("sitemap") if err != nil || len(history) != 3 { t.Fatal("history lost", err) } bad := restored bad.Nodes = append([]MockNode(nil), restored.Nodes...) bad.Nodes[0].ScreenID = "missing" if validateMockGraph(bad, c) == nil { t.Fatal("unknown screen accepted") } bad = restored bad.Edges = append([]DiagramEdge(nil), restored.Edges...) bad.Edges[0].Target = "missing" if validateMockGraph(bad, c) == nil { t.Fatal("dangling edge accepted") } if _, err = s.load("../../escape", g); err == nil { t.Fatal("path escape accepted") } } func TestMockupHTTP(t *testing.T) { store := NewStore(t.TempDir()) mux := newMux(store, NewHub()) for _, path := range []string{"/mockup", "/mockup/sitemap", "/mockup/page-flow", "/mockup/page-flow/W-41", "/mockup/screens", "/mockup/screens/A01", "/api/mockups/catalog", "/api/mockups/graphs/sitemap", "/api/mockups/graphs/W-41"} { w := httptest.NewRecorder() mux.ServeHTTP(w, httptest.NewRequest("GET", path, nil)) if w.Code != 200 { t.Fatal(path, w.Code, w.Body.String()) } } w := httptest.NewRecorder() mux.ServeHTTP(w, httptest.NewRequest("GET", "/api/mockups/graphs/sitemap", nil)) var env mockGraphResponse if err := json.Unmarshal(w.Body.Bytes(), &env); err != nil { t.Fatal(err) } b, _ := json.Marshal(env.Graph) for _, tc := range []struct { origin string want int }{{"", 403}, {"http://evil.example", 403}, {"http://example.com", 200}} { req := httptest.NewRequest("PUT", "/api/mockups/graphs/sitemap", strings.NewReader(string(b))) if tc.origin != "" { req.Header.Set("Origin", tc.origin) } w = httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != tc.want { t.Fatal(tc.origin, w.Code, w.Body.String()) } } for _, path := range []string{"/mockup/screens/MISSING", "/api/mockups/graphs/MISSING", "/mockup-assets/tracker/data/milestones.json"} { w = httptest.NewRecorder() mux.ServeHTTP(w, httptest.NewRequest("GET", path, nil)) if w.Code != 404 { t.Fatal(path, w.Code) } } } func TestMockupConcurrentSavesAndChangedSourcePreserveHistory(t *testing.T) { c, _ := loadMockCatalog("..") g, err := draftMockSitemap("..", c) if err != nil { t.Fatal(err) } s := &mockStore{dir: t.TempDir()} var wg sync.WaitGroup results := make(chan error, 2) for i := 0; i < 2; i++ { wg.Add(1) go func() { defer wg.Done(); _, err := s.save("sitemap", g, c); results <- err }() } wg.Wait() close(results) good, conflicts := 0, 0 for err := range results { if err == nil { good++ } else if err == errMockConflict { conflicts++ } else { t.Fatal(err) } } if good != 1 || conflicts != 1 { t.Fatal(good, conflicts) } newDraft := g newDraft.SourceHash = strings.Repeat("a", 64) loaded, err := s.load("sitemap", newDraft) if err != nil || loaded.SourceHash == newDraft.SourceHash || loaded.Revision != 1 { t.Fatal("source change erased saved graph", err) } history, err := s.history("sitemap") if err != nil || len(history) != 1 { t.Fatal(err) } path := filepath.Join(s.dir, "mockups", "graphs", "sitemap.json") if err = os.WriteFile(path, []byte("broken json"), 0600); err != nil { t.Fatal(err) } if _, err = s.load("sitemap", g); err == nil { t.Fatal("corrupt data silently reset") } } func TestMockupArtifactPathConfinement(t *testing.T) { root := t.TempDir() outside := t.TempDir() if err := os.WriteFile(filepath.Join(outside, "private.svg"), []byte("private"), 0600); err != nil { t.Fatal(err) } if err := os.Symlink(filepath.Join(outside, "private.svg"), filepath.Join(root, "escape.svg")); err != nil { t.Fatal(err) } for _, path := range []string{"../private.svg", "escape.svg", "/etc/passwd", "nested/../secret.svg"} { if _, err := mockFile(root, path); err == nil { t.Fatal("allowed path escape", path) } } } func TestFullModuleLabelsAndFlowGroups(t *testing.T) { c, err := loadMockCatalog("..") if err != nil { t.Fatal(err) } uc, err := loadUseCases("..") if err != nil { t.Fatal(err) } for code, m := range uc.Modules { if c.ModuleNames[code] != m.Name { t.Errorf("missing full label for %s", code) } } for _, s := range c.Screens { if s.ModuleName == "" || s.ModuleName == s.Module { t.Errorf("abbreviated label for %s", s.ID) } } if moduleLabel(c.ModuleNames, "UNKNOWN") == "UNKNOWN" { t.Fatal("unknown module leaked abbreviation") } mux := newMux(NewStore(t.TempDir()), NewHub()) w := httptest.NewRecorder() mux.ServeHTTP(w, httptest.NewRequest("GET", "/mockup/page-flow", nil)) if w.Code != 200 { t.Fatal(w.Code) } html := w.Body.String() if strings.Count(html, "data-flow-search=") != 162 || !strings.Contains(html, `data-flow-module="ADM"`) || !strings.Contains(html, "รับสมัครและคัดเลือก") || !strings.Contains(html, "data-flow-collapse-all") { t.Fatal("module folding or rows missing") } }