All checks were successful
Build and Test / build-and-test (push) Successful in 38s
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>
129 lines
3.4 KiB
Go
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")
|
|
}
|
|
}
|