Files
todo-hetzner/app/internal/handler/handler_test.go
T
cole-maxwell1 94d18ff8e2 fix(handler): preserve origin on form submissions
Use the same-origin referrer policy to prevent Chrome from sending a null Origin header on POST requests.
2026-08-07 15:30:49 -05:00

134 lines
4.0 KiB
Go

package handler
import (
"context"
"errors"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"todo-hetzner/app/internal/model"
)
type fakeStore struct {
todos []model.Todo
created string
toggled int64
deleted int64
pingErr error
}
func (s *fakeStore) List(context.Context) ([]model.Todo, error) { return s.todos, nil }
func (s *fakeStore) Create(_ context.Context, title string) error {
s.created = title
return nil
}
func (s *fakeStore) Toggle(_ context.Context, id int64) (bool, error) {
s.toggled = id
return id != 404, nil
}
func (s *fakeStore) Delete(_ context.Context, id int64) (bool, error) {
s.deleted = id
return id != 404, nil
}
func (s *fakeStore) Ping(context.Context) error { return s.pingErr }
func testHandler(t *testing.T, store *fakeStore) http.Handler {
t.Helper()
h, err := New(store, slog.New(slog.NewTextHandler(io.Discard, nil)))
if err != nil {
t.Fatal(err)
}
return h.Routes()
}
func TestListRendersEscapedTodos(t *testing.T) {
router := testHandler(t, &fakeStore{todos: []model.Todo{{ID: 1, Title: "<script>alert(1)</script>"}}})
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/", nil))
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d", recorder.Code)
}
if strings.Contains(recorder.Body.String(), "<script>") || !strings.Contains(recorder.Body.String(), "&lt;script&gt;") {
t.Fatalf("title was not escaped: %s", recorder.Body.String())
}
if got := recorder.Header().Get("Referrer-Policy"); got != "same-origin" {
t.Fatalf("Referrer-Policy = %q, want %q", got, "same-origin")
}
}
func TestCreateValidatesAndTrimsTitle(t *testing.T) {
store := &fakeStore{}
router := testHandler(t, store)
form := url.Values{"title": {" ship it "}}
req := httptest.NewRequest(http.MethodPost, "/todos", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, req)
if recorder.Code != http.StatusSeeOther || store.created != "ship it" {
t.Fatalf("status = %d, created = %q", recorder.Code, store.created)
}
req = httptest.NewRequest(http.MethodPost, "/todos", strings.NewReader("title=++"))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
recorder = httptest.NewRecorder()
router.ServeHTTP(recorder, req)
if recorder.Code != http.StatusUnprocessableEntity {
t.Fatalf("empty title status = %d", recorder.Code)
}
}
func TestMutationsAndNotFound(t *testing.T) {
store := &fakeStore{}
router := testHandler(t, store)
for _, tc := range []struct {
path string
want int
}{
{"/todos/12/toggle", http.StatusSeeOther},
{"/todos/13/delete", http.StatusSeeOther},
{"/todos/404/delete", http.StatusNotFound},
{"/todos/nope/toggle", http.StatusBadRequest},
} {
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, httptest.NewRequest(http.MethodPost, tc.path, nil))
if recorder.Code != tc.want {
t.Errorf("%s status = %d, want %d", tc.path, recorder.Code, tc.want)
}
}
if store.toggled != 12 || store.deleted != 404 {
t.Fatalf("toggled = %d, deleted = %d", store.toggled, store.deleted)
}
}
func TestHealth(t *testing.T) {
store := &fakeStore{}
router := testHandler(t, store)
for _, tc := range []struct {
err error
want int
}{{nil, http.StatusOK}, {errors.New("down"), http.StatusServiceUnavailable}} {
store.pingErr = tc.err
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/healthz", nil))
if recorder.Code != tc.want {
t.Errorf("status = %d, want %d", recorder.Code, tc.want)
}
}
}
func TestRejectsCrossOriginMutation(t *testing.T) {
router := testHandler(t, &fakeStore{})
req := httptest.NewRequest(http.MethodPost, "/todos/1/delete", nil)
req.Header.Set("Origin", "https://attacker.example")
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, req)
if recorder.Code != http.StatusForbidden {
t.Fatalf("status = %d", recorder.Code)
}
}