Files
paliad/internal/auth/auth_test.go
m 3da11bd798 chore(t-paliad-081): doc + dead-code batch (F-5/F-10/F-11/F-15/F-16/F-17/F-18)
Bundle of small audit findings, all doc-only or dead-code:

- F-5: refresh stale escalation-contact comment in models.User —
  Settings UI dropdown shipped 2026-04-29 (t-paliad-066).
- F-10: add "OBSOLETED by migration 018" note to migrations 004/005/006
  so readers stop hunting for the live shape in obsolete files.
- F-11: document the data-loss semantics of dropping
  paliad.partner_unit_events on the 027 down — audit rows are
  append-only telemetry, accepted loss on rollback.
- F-15: drop the patholo_session / patholo_refresh cookie fallback
  added during the 2026-04-16 rebrand. Active users have long since
  been re-authed through the upgrade path; inactive users hit the
  normal /login flow.
- F-16: refresh stale /api/departments comment in team_pages.go to
  /api/partner-units (renamed in t-paliad-070).
- F-17: move internal/db/migrations/_dev/mock_supabase_auth.sql to
  internal/db/devtools/ so a future loosening of the //go:embed
  pattern can't accidentally ship the dev-only fixture.
- F-18: update docs/project-status.md "Audit polish-2" entry — the
  batch shipped via t-paliad-067 / 068 / 073, follow-ups are now
  tracked under the 2026-04-30 re-audit + t-paliad-074.

go build / vet / test clean.
2026-04-30 03:42:25 +02:00

112 lines
3.1 KiB
Go

package auth
import (
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
)
// testSecret mirrors the format of a Supabase JWT signing key.
var testSecret = []byte("test-secret-for-hs256-verification-123")
func sign(t *testing.T, secret []byte, claims jwt.MapClaims) string {
t.Helper()
tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
s, err := tok.SignedString(secret)
if err != nil {
t.Fatalf("sign: %v", err)
}
return s
}
func TestVerifyToken_Valid(t *testing.T) {
c := &Client{JWTSecret: testSecret}
token := sign(t, testSecret, jwt.MapClaims{
"sub": "11111111-1111-1111-1111-111111111111",
"exp": time.Now().Add(time.Hour).Unix(),
})
got, err := c.VerifyToken(token)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got.Sub != "11111111-1111-1111-1111-111111111111" {
t.Errorf("sub: got %q", got.Sub)
}
}
func TestVerifyToken_WrongSecret(t *testing.T) {
c := &Client{JWTSecret: testSecret}
token := sign(t, []byte("attacker-guessed-wrong"), jwt.MapClaims{
"sub": "11111111-1111-1111-1111-111111111111",
"exp": time.Now().Add(time.Hour).Unix(),
})
if _, err := c.VerifyToken(token); err == nil {
t.Fatal("expected error for wrong-signature token, got nil (auth bypass)")
}
}
func TestVerifyToken_Expired(t *testing.T) {
c := &Client{JWTSecret: testSecret}
token := sign(t, testSecret, jwt.MapClaims{
"sub": "11111111-1111-1111-1111-111111111111",
"exp": time.Now().Add(-time.Hour).Unix(),
})
_, err := c.VerifyToken(token)
if err == nil {
t.Fatal("expected error for expired token")
}
if !errors.Is(err, jwt.ErrTokenExpired) {
t.Errorf("expected ErrTokenExpired, got %v", err)
}
}
func TestVerifyToken_AlgNone(t *testing.T) {
c := &Client{JWTSecret: testSecret}
// An attacker might try alg=none to bypass signature checks.
tok := jwt.NewWithClaims(jwt.SigningMethodNone, jwt.MapClaims{
"sub": "22222222-2222-2222-2222-222222222222",
"exp": time.Now().Add(time.Hour).Unix(),
})
token, err := tok.SignedString(jwt.UnsafeAllowNoneSignatureType)
if err != nil {
t.Fatalf("sign none: %v", err)
}
if _, err := c.VerifyToken(token); err == nil {
t.Fatal("expected error for alg=none, got nil (critical bypass)")
}
}
func TestVerifyToken_MissingSub(t *testing.T) {
c := &Client{JWTSecret: testSecret}
token := sign(t, testSecret, jwt.MapClaims{
"exp": time.Now().Add(time.Hour).Unix(),
})
if _, err := c.VerifyToken(token); err == nil {
t.Fatal("expected error for missing sub claim")
}
}
func TestVerifyToken_Garbage(t *testing.T) {
c := &Client{JWTSecret: testSecret}
if _, err := c.VerifyToken("not.a.jwt"); err == nil {
t.Fatal("expected error for garbage token")
}
}
func TestMiddleware_NoCookieRejected(t *testing.T) {
c := &Client{JWTSecret: testSecret}
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("next should not run without auth")
})
req := httptest.NewRequest("GET", "/api/anything", nil)
rec := httptest.NewRecorder()
c.Middleware(next).ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("expected 401 on API without cookies, got %d", rec.Code)
}
}