hsa-app/internal/web/web_test.go
Jean-Michel Tremblay 866bc175fb
All checks were successful
Build and Test / build-and-test (push) Successful in 38s
Security hardening: server-side session expiry + nosniff on files
1. Enforce the 12h session lifetime server-side in requireAuth (reject a
   session older than the TTL even if the sealed cookie is intact), so a
   leaked cookie value can't be replayed indefinitely. Shared sessionTTL
   const drives both the cookie MaxAge and the check.
2. Send X-Content-Type-Options: nosniff when serving user-uploaded
   receipt/attachment bytes, so the browser won't sniff past the declared
   (upload-time allowlisted) MIME type.

Update SPEC §2 and §6 accordingly; tests cover stale-session rejection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:56:38 -04:00

129 lines
3.4 KiB
Go

package web
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"maisym.com/hsa/internal/auth"
"maisym.com/hsa/internal/config"
)
func testServer() *Server {
var key [32]byte
copy(key[:], "web-test-key-32-bytes-padded!!!!")
return &Server{cfg: config.Config{
SessionKey: key,
RequiredGroup: "hsa-users",
SecureCookies: false,
}}
}
func TestStaticCSS(t *testing.T) {
s := testServer()
req := httptest.NewRequest(http.MethodGet, "/static/style.css", nil)
rec := httptest.NewRecorder()
s.Routes().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", rec.Code)
}
if !strings.Contains(rec.Body.String(), "font-family") {
t.Errorf("CSS not served: %q", rec.Body.String())
}
}
func TestHealthz(t *testing.T) {
s := testServer()
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
rec := httptest.NewRecorder()
s.Routes().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", rec.Code)
}
if !strings.Contains(rec.Body.String(), "ok") {
t.Errorf("body = %q, want to contain ok", rec.Body.String())
}
}
func TestHome_NoSession_RedirectsToLogin(t *testing.T) {
s := testServer()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
s.Routes().ServeHTTP(rec, req)
if rec.Code != http.StatusFound {
t.Fatalf("status = %d, want 302", rec.Code)
}
if loc := rec.Header().Get("Location"); loc != "/login" {
t.Errorf("Location = %q, want /login", loc)
}
}
func TestHome_ValidSession_ShowsUploadForm(t *testing.T) {
s := testServerWithStore(t)
sess := auth.Session{Subject: "jm@example.com", Groups: []string{"hsa-users"}, IssuedAt: time.Now()}
cookie, err := auth.EncodeSession(sess, s.cfg.SessionKey)
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: sessionCookie, Value: cookie})
rec := httptest.NewRecorder()
s.Routes().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", rec.Code)
}
body := rec.Body.String()
if !strings.Contains(body, "jm@example.com") {
t.Errorf("body missing subject: %q", body)
}
if !strings.Contains(body, "Add receipt") {
t.Errorf("body missing upload form: %q", body)
}
}
func TestHome_TamperedSession_RedirectsToLogin(t *testing.T) {
s := testServer()
sess := auth.Session{Subject: "jm@example.com", Groups: []string{"hsa-users"}}
cookie, _ := auth.EncodeSession(sess, s.cfg.SessionKey)
tampered := cookie[:len(cookie)-3] + "AAA"
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: sessionCookie, Value: tampered})
rec := httptest.NewRecorder()
s.Routes().ServeHTTP(rec, req)
if rec.Code != http.StatusFound {
t.Fatalf("status = %d, want 302", rec.Code)
}
if loc := rec.Header().Get("Location"); loc != "/login" {
t.Errorf("Location = %q, want /login", loc)
}
}
func TestLogout_ClearsCookieAndRedirects(t *testing.T) {
s := testServer()
req := httptest.NewRequest(http.MethodGet, "/logout", nil)
rec := httptest.NewRecorder()
s.Routes().ServeHTTP(rec, req)
if rec.Code != http.StatusFound {
t.Fatalf("status = %d, want 302", rec.Code)
}
// The session cookie should be expired (MaxAge < 0).
var cleared bool
for _, c := range rec.Result().Cookies() {
if c.Name == sessionCookie && c.MaxAge < 0 {
cleared = true
}
}
if !cleared {
t.Error("logout did not clear the session cookie")
}
}