diff options
Diffstat (limited to 'internal/submit/types_test.go')
| -rw-r--r-- | internal/submit/types_test.go | 125 |
1 files changed, 124 insertions, 1 deletions
diff --git a/internal/submit/types_test.go b/internal/submit/types_test.go index a2a7c1a..16a2b84 100644 --- a/internal/submit/types_test.go +++ b/internal/submit/types_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "net/url" "strings" "testing" "testing/fstest" @@ -15,13 +16,135 @@ import ( func newTestApp(t *testing.T) *App { t.Helper() - app, err := NewApp(config.Config{}, nil, storage.NewReplayCache(time.Minute), fstest.MapFS{}, "<!doctype html>") + app, err := NewApp(config.Config{}, nil, nil, storage.NewReplayCache(time.Minute), fstest.MapFS{}, "<!doctype html>") if err != nil { t.Fatalf("NewApp returned error: %v", err) } return app } +func TestCSRFCookieHasNoElapsedTimeLimit(t *testing.T) { + app := newTestApp(t) + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + + app.handleIndex(rec, req) + + cookies := rec.Result().Cookies() + if len(cookies) != 1 { + t.Fatalf("unexpected cookie count: got %d want 1", len(cookies)) + } + cookie := cookies[0] + if cookie.Name != "m2u_csrf" { + t.Fatalf("unexpected cookie name: %q", cookie.Name) + } + if cookie.MaxAge != 0 || !cookie.Expires.IsZero() { + t.Fatalf("CSRF cookie has an elapsed-time expiry: MaxAge=%d Expires=%v", cookie.MaxAge, cookie.Expires) + } + if !cookie.HttpOnly || cookie.SameSite != http.SameSiteStrictMode { + t.Fatal("CSRF cookie security attributes were weakened") + } +} + +func TestCSRFRefreshIssuesValidToken(t *testing.T) { + app := newTestApp(t) + req := httptest.NewRequest(http.MethodGet, "/csrf", nil) + rec := httptest.NewRecorder() + + app.handleCSRF(rec, req) + + if got := rec.Header().Get("Cache-Control"); got != "no-store" { + t.Fatalf("unexpected Cache-Control: %q", got) + } + var body map[string]string + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("decode CSRF response: %v", err) + } + token := body["token"] + if token == "" { + t.Fatal("CSRF refresh returned an empty token") + } + cookies := rec.Result().Cookies() + if len(cookies) != 1 { + t.Fatalf("unexpected cookie count: got %d want 1", len(cookies)) + } + + form := url.Values{"csrf_token": {token}} + post := httptest.NewRequest(http.MethodPost, "/submit", strings.NewReader(form.Encode())) + post.Header.Set("Content-Type", "application/x-www-form-urlencoded") + post.AddCookie(cookies[0]) + if err := post.ParseForm(); err != nil { + t.Fatalf("parse form: %v", err) + } + if !app.verifyCSRF(post) { + t.Fatal("refreshed CSRF token did not verify") + } +} + +func TestCSRFCookieSurvivesRestartWithPersistentKey(t *testing.T) { + cfg := config.Config{Security: config.SecurityConfig{CSRFKey: []byte(strings.Repeat("k", 32))}} + newApp := func() *App { + app, err := NewApp(cfg, nil, nil, storage.NewReplayCache(time.Minute), fstest.MapFS{}, "<!doctype html>") + if err != nil { + t.Fatalf("NewApp returned error: %v", err) + } + return app + } + + first := httptest.NewRecorder() + newApp().handleCSRF(first, httptest.NewRequest(http.MethodGet, "/csrf", nil)) + var firstBody map[string]string + if err := json.Unmarshal(first.Body.Bytes(), &firstBody); err != nil { + t.Fatalf("decode first CSRF response: %v", err) + } + cookies := first.Result().Cookies() + if len(cookies) != 1 { + t.Fatalf("unexpected cookie count: %d", len(cookies)) + } + + requestAfterRestart := httptest.NewRequest(http.MethodGet, "/csrf", nil) + requestAfterRestart.AddCookie(cookies[0]) + second := httptest.NewRecorder() + newApp().handleCSRF(second, requestAfterRestart) + var secondBody map[string]string + if err := json.Unmarshal(second.Body.Bytes(), &secondBody); err != nil { + t.Fatalf("decode second CSRF response: %v", err) + } + if secondBody["token"] != firstBody["token"] { + t.Fatal("restart rotated a still-valid session token") + } + if got := len(second.Result().Cookies()); got != 0 { + t.Fatalf("valid session unexpectedly rotated %d cookies", got) + } +} + +type transportStateStub struct { + ready bool +} + +func (s *transportStateStub) Ready() bool { return s.ready } +func (s *transportStateStub) MarkSuccess() { s.ready = true } +func (s *transportStateStub) MarkFailure(error) { s.ready = false } + +func TestReadinessUsesCachedTransportState(t *testing.T) { + app := newTestApp(t) + state := &transportStateStub{} + app.transport = state + + recorder := httptest.NewRecorder() + app.handleReadiness(recorder, httptest.NewRequest(http.MethodGet, "/readyz", nil)) + if recorder.Code != http.StatusServiceUnavailable { + t.Fatalf("unready status = %d, want %d", recorder.Code, http.StatusServiceUnavailable) + } + + state.ready = true + recorder = httptest.NewRecorder() + app.handleReadiness(recorder, httptest.NewRequest(http.MethodGet, "/readyz", nil)) + if recorder.Code != http.StatusOK { + t.Fatalf("ready status = %d, want %d", recorder.Code, http.StatusOK) + } +} + func TestSuccessResponseJSONNoStore(t *testing.T) { app := newTestApp(t) req := httptest.NewRequest(http.MethodPost, "/submit", nil) |
