mirror of
https://github.com/cole-maxwell1/todo-hetzner.git
synced 2026-10-27 19:02:49 -05:00
Use the same-origin referrer policy to prevent Chrome from sending a null Origin header on POST requests.
134 lines
4.0 KiB
Go
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(), "<script>") {
|
|
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)
|
|
}
|
|
}
|