package handlers import ( "context" "html/template" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "github.com/go-chi/chi/v5" ) func setupDocsTestHandler(t *testing.T) *Handler { t.Helper() h, cleanup := setupTestHandler(t) t.Cleanup(cleanup) docsDir := t.TempDir() if err := os.MkdirAll(filepath.Join(docsDir, "specs"), 0755); err != nil { t.Fatalf("failed to create specs dir: %v", err) } if err := os.MkdirAll(filepath.Join(docsDir, "plans"), 0755); err != nil { t.Fatalf("failed to create plans dir: %v", err) } if err := os.WriteFile(filepath.Join(docsDir, "specs", "2026-07-14-example-design.md"), []byte("# Example\n\nSome body text."), 0644); err != nil { t.Fatalf("failed to write fixture: %v", err) } h.config.DocsDir = docsDir return h } func TestHandleDocsIndex_ListsWhitelistedCategories(t *testing.T) { h := setupDocsTestHandler(t) req := httptest.NewRequest("GET", "/docs", nil) w := httptest.NewRecorder() h.HandleDocsIndex(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } mock := h.renderer.(*MockRenderer) if len(mock.Calls) != 1 { t.Fatalf("expected 1 render call, got %d", len(mock.Calls)) } if mock.Calls[0].Name != "docs-index.html" { t.Errorf("template name = %q, want %q", mock.Calls[0].Name, "docs-index.html") } data, ok := mock.Calls[0].Data.(struct{ Categories []docsCategoryListing }) if !ok { t.Fatalf("unexpected data type: %T", mock.Calls[0].Data) } if len(data.Categories) != 2 { t.Fatalf("expected 2 categories (specs, plans), got %d", len(data.Categories)) } specs := data.Categories[0] if specs.Name != "specs" { t.Fatalf("expected first category 'specs', got %q", specs.Name) } if len(specs.Entries) != 1 || specs.Entries[0].Name != "2026-07-14-example-design.md" { t.Fatalf("expected 1 spec entry, got %+v", specs.Entries) } if specs.Entries[0].URL != "/docs/specs/2026-07-14-example-design.md" { t.Errorf("URL = %q, want %q", specs.Entries[0].URL, "/docs/specs/2026-07-14-example-design.md") } plans := data.Categories[1] if plans.Name != "plans" || len(plans.Entries) != 0 { t.Fatalf("expected empty plans category, got %+v", plans) } } func routeWithParams(method, target, category, filename string) *http.Request { req := httptest.NewRequest(method, target, nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("category", category) rctx.URLParams.Add("filename", filename) return req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) } func TestHandleDocsView_RendersMarkdownFile(t *testing.T) { h := setupDocsTestHandler(t) req := routeWithParams("GET", "/docs/specs/2026-07-14-example-design.md", "specs", "2026-07-14-example-design.md") w := httptest.NewRecorder() h.HandleDocsView(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } mock := h.renderer.(*MockRenderer) if len(mock.Calls) != 1 || mock.Calls[0].Name != "docs-view.html" { t.Fatalf("expected 1 call to docs-view.html, got %+v", mock.Calls) } data, ok := mock.Calls[0].Data.(struct { Title string Body template.HTML }) if !ok { t.Fatalf("unexpected data type: %T", mock.Calls[0].Data) } if data.Title != "2026-07-14-example-design.md" { t.Errorf("Title = %q, want %q", data.Title, "2026-07-14-example-design.md") } if !strings.Contains(string(data.Body), "

Example

") { t.Errorf("Body does not contain expected rendered heading: %s", data.Body) } if !strings.Contains(string(data.Body), "Some body text.") { t.Errorf("Body does not contain expected paragraph text: %s", data.Body) } } func TestHandleDocsView_RejectsUnknownCategory(t *testing.T) { h := setupDocsTestHandler(t) req := routeWithParams("GET", "/docs/other/2026-07-14-example-design.md", "other", "2026-07-14-example-design.md") w := httptest.NewRecorder() h.HandleDocsView(w, req) if w.Code != http.StatusNotFound { t.Errorf("expected 404 for unknown category, got %d", w.Code) } } func TestHandleDocsView_RejectsNonMarkdownFilename(t *testing.T) { h := setupDocsTestHandler(t) req := routeWithParams("GET", "/docs/specs/..", "specs", "..") w := httptest.NewRecorder() h.HandleDocsView(w, req) if w.Code != http.StatusNotFound { t.Errorf("expected 404 for a non-.md filename (e.g. '..'), got %d", w.Code) } } func TestHandleDocsView_RejectsMissingFile(t *testing.T) { h := setupDocsTestHandler(t) req := routeWithParams("GET", "/docs/specs/does-not-exist.md", "specs", "does-not-exist.md") w := httptest.NewRecorder() h.HandleDocsView(w, req) if w.Code != http.StatusNotFound { t.Errorf("expected 404 for a missing file, got %d", w.Code) } }