hsa-app/internal/web/web_test.go
Jean-Michel Tremblay 8b7252dad4 Initial commit: HSA receipt tracker
Go app for capturing and archiving HSA-eligible receipts: OIDC/PKCE auth
against Authelia, SQLite storage with dual-write (filesystem + DB blob),
mobile-first upload, and DB export.

Adds AI receipt classification: a config.json catalog of people and
categories (seeded into the DB on startup), a prompt builder that derives
name-order/initial variants from the data (with same-surname ambiguity
handling), and an Anthropic tool-use client behind POST /classify. Tests
run against a mock endpoint; a live integration test is env-gated to the
cheapest model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:40:12 -04:00

128 lines
3.4 KiB
Go

package web
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"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"}}
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")
}
}