From 16ff83627442c5bd0a23a728bdee92eef44367a7 Mon Sep 17 00:00:00 2001 From: adminoo Date: Mon, 2 Feb 2026 10:09:01 +0100 Subject: [PATCH] feat: tests for http handlers and render package --- internal/render/render_test.go | 93 +++++++++++++ internal/web/handler_test.go | 234 +++++++++++++++++++++++++++++++++ 2 files changed, 327 insertions(+) create mode 100644 internal/web/handler_test.go diff --git a/internal/render/render_test.go b/internal/render/render_test.go index 90605f0..1158653 100644 --- a/internal/render/render_test.go +++ b/internal/render/render_test.go @@ -1,6 +1,10 @@ package render import ( + "net/http" + "net/http/httptest" + "os" + "path/filepath" "strings" "testing" ) @@ -50,3 +54,92 @@ Check this out [here](http://tatata.toto) } } } + +func TestTemplateManagerGetTemplate_Caches(t *testing.T) { + baseDir := t.TempDir() + writeTemplate(t, baseDir, "base.tmpl", `{{ define "base" }}base{{ end }}`) + writeTemplate(t, baseDir, "index.tmpl", `{{ define "content" }}index{{ end }}`) + + tm := NewTemplateManager(baseDir) + td := &TemplateData{ + Name: "index", + FileNameSet: []string{"base", "index"}, + } + + first, err := tm.GetTemplate(td) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + second, err := tm.GetTemplate(td) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if first != second { + t.Fatalf("expected cached template instance") + } +} + +func TestTemplateManagerGetTemplate_Missing(t *testing.T) { + baseDir := t.TempDir() + tm := NewTemplateManager(baseDir) + + td := &TemplateData{ + Name: "missing", + FileNameSet: []string{"missing"}, + } + + if _, err := tm.GetTemplate(td); err == nil { + t.Fatalf("expected error for missing template") + } +} + +func TestTemplateManagerRender(t *testing.T) { + baseDir := t.TempDir() + writeTemplate(t, baseDir, "base.tmpl", `{{ define "base" }}{{ template "content" . }}{{ end }}`) + writeTemplate(t, baseDir, "noteList.tmpl", `{{ define "noteList" }}notes{{ end }}`) + writeTemplate(t, baseDir, "index.tmpl", `{{ define "content" }}hello{{ end }}`) + + tm := NewTemplateManager(baseDir) + rec := httptest.NewRecorder() + + err := tm.Render(rec, "index", map[string]string{"msg": "hi"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if ct := rec.Header().Get("Content-Type"); ct != "text/html; charset=utf-8" { + t.Fatalf("expected content-type text/html; charset=utf-8, got %q", ct) + } + + if rec.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rec.Code) + } + + if !strings.Contains(rec.Body.String(), "hello") { + t.Fatalf("expected rendered template content") + } +} + +func TestTemplateManagerRender_MissingTemplate(t *testing.T) { + baseDir := t.TempDir() + writeTemplate(t, baseDir, "base.tmpl", `{{ define "base" }}{{ template "content" . }}{{ end }}`) + writeTemplate(t, baseDir, "noteList.tmpl", `{{ define "noteList" }}notes{{ end }}`) + + tm := NewTemplateManager(baseDir) + rec := httptest.NewRecorder() + + if err := tm.Render(rec, "index", nil); err == nil { + t.Fatalf("expected error for missing template") + } +} + +func writeTemplate(t *testing.T, dir, name, contents string) { + t.Helper() + + path := filepath.Join(dir, name) + if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { + t.Fatalf("write template %s: %v", name, err) + } +} diff --git a/internal/web/handler_test.go b/internal/web/handler_test.go new file mode 100644 index 0000000..01227ed --- /dev/null +++ b/internal/web/handler_test.go @@ -0,0 +1,234 @@ +package web + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "donniemarko/internal/note" + "donniemarko/internal/render" + "donniemarko/internal/service" + "donniemarko/internal/storage" +) + +type testEnv struct { + handler *Handler + notes map[string]*note.Note +} + +func newTestEnv(t *testing.T) *testEnv { + t.Helper() + + ns := storage.NewNoteStorage() + + notes := []*note.Note{ + { + ID: "a1", + Title: "Alpha", + Content: "# Alpha\ncontent", + UpdatedAt: time.Date(2025, 12, 31, 10, 0, 0, 0, time.UTC), + }, + { + ID: "b2", + Title: "Beta", + Content: "# Beta\nRust tips", + UpdatedAt: time.Date(2026, 1, 1, 10, 0, 0, 0, time.UTC), + }, + { + ID: "z9", + Title: "Zulu", + Content: "# Zulu\nRust book", + UpdatedAt: time.Date(2026, 1, 2, 10, 0, 0, 0, time.UTC), + }, + } + + for _, n := range notes { + if err := ns.Create(n); err != nil { + t.Fatalf("create note: %v", err) + } + } + + svc := service.NewNoteService() + svc.SetStorage(ns) + + tm := render.NewTemplateManager("templates") + handler := NewHandler(svc, tm) + + noteMap := map[string]*note.Note{} + for _, n := range notes { + noteMap[n.ID] = n + } + + return &testEnv{ + handler: handler, + notes: noteMap, + } +} + +func assertOrder(t *testing.T, body string, want []string) { + t.Helper() + + prev := -1 + for _, title := range want { + idx := strings.Index(body, title) + if idx == -1 { + t.Fatalf("expected to find %q in response body", title) + } + if idx <= prev { + t.Fatalf("expected %q to appear after previous title", title) + } + prev = idx + } +} + +func noteMarker(id string) string { + return `data-hash="` + id + `"` +} + +func TestHandlerRoot_DefaultSortRecent(t *testing.T) { + env := newTestEnv(t) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + + env.handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rec.Code) + } + + if ct := rec.Header().Get("Content-Type"); ct != "text/html; charset=utf-8" { + t.Fatalf("expected content-type text/html; charset=utf-8, got %q", ct) + } + + body := rec.Body.String() + assertOrder(t, body, []string{noteMarker("z9"), noteMarker("b2"), noteMarker("a1")}) +} + +func TestHandlerRoot_SortAlpha(t *testing.T) { + env := newTestEnv(t) + + req := httptest.NewRequest(http.MethodGet, "/?sort=alpha", nil) + rec := httptest.NewRecorder() + + env.handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rec.Code) + } + + body := rec.Body.String() + assertOrder(t, body, []string{noteMarker("a1"), noteMarker("b2"), noteMarker("z9")}) +} + +func TestHandlerRoot_Search(t *testing.T) { + env := newTestEnv(t) + + req := httptest.NewRequest(http.MethodGet, "/?search=rust&sort=alpha", nil) + rec := httptest.NewRecorder() + + env.handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rec.Code) + } + + body := rec.Body.String() + if !strings.Contains(body, "Matching results for query 'rust'") { + t.Fatalf("expected search header to be rendered") + } + + if strings.Contains(body, `data-hash="a1"`) { + t.Fatalf("expected non-matching note to be excluded") + } + + assertOrder(t, body, []string{noteMarker("b2"), noteMarker("z9")}) +} + +func TestHandlerNotes_Success(t *testing.T) { + env := newTestEnv(t) + + req := httptest.NewRequest(http.MethodGet, "/notes/b2", nil) + rec := httptest.NewRecorder() + + env.handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rec.Code) + } + + body := rec.Body.String() + if !strings.Contains(body, "

Beta

") { + t.Fatalf("expected rendered markdown for note content") + } + if !strings.Contains(body, `class="active-note"`) { + t.Fatalf("expected active note class in list") + } + if !strings.Contains(body, `data-hash="b2"`) { + t.Fatalf("expected active note hash in list") + } +} + +func TestHandlerNotes_NotFound(t *testing.T) { + env := newTestEnv(t) + + req := httptest.NewRequest(http.MethodGet, "/notes/missing", nil) + rec := httptest.NewRecorder() + + env.handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusNotFound { + t.Fatalf("expected status 404, got %d", rec.Code) + } + + if !strings.Contains(rec.Body.String(), "Note not found") { + t.Fatalf("expected not found message") + } +} + +func TestHandler_NotFoundPath(t *testing.T) { + env := newTestEnv(t) + + req := httptest.NewRequest(http.MethodGet, "/nope", nil) + rec := httptest.NewRecorder() + + env.handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusNotFound { + t.Fatalf("expected status 404, got %d", rec.Code) + } +} + +func TestExtractHash(t *testing.T) { + cases := []struct { + name string + path string + want string + }{ + { + name: "valid note path", + path: "/notes/abcd", + want: "abcd", + }, + { + name: "missing hash", + path: "/notes/", + want: "", + }, + { + name: "wrong prefix", + path: "/other/abcd", + want: "", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := extractHash(tc.path); got != tc.want { + t.Fatalf("expected %q, got %q", tc.want, got) + } + }) + } +}