summaryrefslogtreecommitdiff
path: root/internal/handlers/docs_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/handlers/docs_test.go')
-rw-r--r--internal/handlers/docs_test.go154
1 files changed, 154 insertions, 0 deletions
diff --git a/internal/handlers/docs_test.go b/internal/handlers/docs_test.go
new file mode 100644
index 0000000..9d684d8
--- /dev/null
+++ b/internal/handlers/docs_test.go
@@ -0,0 +1,154 @@
+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), "<h1>Example</h1>") {
+ 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)
+ }
+}