package submit import ( "encoding/json" "net/http" "net/http/httptest" "net/url" "strings" "testing" "testing/fstest" "time" "n2usenet/internal/config" "n2usenet/internal/storage" ) func newTestApp(t *testing.T) *App { t.Helper() app, err := NewApp(config.Config{}, nil, nil, storage.NewReplayCache(time.Minute), fstest.MapFS{}, "") 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{}, "") 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) req.Header.Set("Accept", "application/json") rec := httptest.NewRecorder() app.successResponse(rec, req, "") if rec.Code != http.StatusOK { t.Fatalf("unexpected status: got %d want %d", rec.Code, http.StatusOK) } if got := rec.Header().Get("Cache-Control"); got != "no-store" { t.Fatalf("unexpected Cache-Control: %q", got) } if got := rec.Header().Get("Clear-Site-Data"); got != "\"storage\"" { t.Fatalf("unexpected Clear-Site-Data: %q", got) } if got := rec.Header().Get("Content-Type"); !strings.Contains(got, "application/json") { t.Fatalf("unexpected Content-Type: %q", got) } var body map[string]string if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("failed to decode response body: %v", err) } if body["messageId"] != "" { t.Fatalf("unexpected messageId: %q", body["messageId"]) } } func TestErrorResponseJSONNoStore(t *testing.T) { app := newTestApp(t) req := httptest.NewRequest(http.MethodPost, "/submit", nil) req.Header.Set("Accept", "application/json") rec := httptest.NewRecorder() app.errorResponse(rec, req, http.StatusBadRequest, "Validation failed.") if rec.Code != http.StatusBadRequest { t.Fatalf("unexpected status: got %d want %d", rec.Code, http.StatusBadRequest) } if got := rec.Header().Get("Cache-Control"); got != "no-store" { t.Fatalf("unexpected Cache-Control: %q", got) } if got := rec.Header().Get("Content-Type"); !strings.Contains(got, "application/json") { t.Fatalf("unexpected Content-Type: %q", got) } var body map[string]string if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("failed to decode response body: %v", err) } if body["error"] != "Validation failed." { t.Fatalf("unexpected error body: %q", body["error"]) } }