NextMyDay/internal/core/httpx/presession_test.go
Claus Lohmar d11e14295b feat(core): add the store, identity plumbing and the door (SPEC-003/004)
- store: SQLite (WAL, foreign keys ON, CGO-free driver) + the embedded
  migration runner; each module's migrations are name-prefixed (D-012)
- audit: the append-only audit log; the actor threads via context (R-1)
- users: the user_id/GUID/email anchors (D-013)
- config: the YAML settings file (D-035) + validation
- httpx: cookies · loopback detection · trusted-proxy XFF · CSRF · security
  headers · redaction
- middleware: correlation id · audit · rate limiting
- mail: the invite/recovery mailer seam
- server: the core DOOR — the resolution switch, the dispatch registry
  (RegisterModule), the role gates, /setup · enroll/login · recovery flows,
  the reserved `static` workflow, and the loopback-only bootstrap (D-025)
2026-10-14 10:53:51 +00:00

87 lines
2.5 KiB
Go

package httpx_test
import (
"net/http/httptest"
"testing"
"git.lohmar.co.uk/cclohmar/NextMyDay/internal/core/httpx"
)
func TestSignerRoundTripAndTamper(t *testing.T) {
s, err := httpx.NewSigner([]byte("a-signing-key-0123456789abcdef"))
if err != nil {
t.Fatalf("NewSigner: %v", err)
}
if _, err := httpx.NewSigner(nil); err == nil {
t.Error("NewSigner(nil) = nil, want an error (fail closed)")
}
const value = "hello.world/with/slashes"
tok := s.Sign(value)
got, ok := s.Unsign(tok)
if !ok || got != value {
t.Fatalf("Unsign(Sign(%q)) = %q,%v", value, got, ok)
}
if _, ok := s.Unsign(tok + "x"); ok {
t.Error("Unsign accepted a tampered token")
}
if _, ok := s.Unsign("not-a-token"); ok {
t.Error("Unsign accepted a malformed token")
}
// A different key must not validate the token.
other, _ := httpx.NewSigner([]byte("another-key-0123456789abcdefgh"))
if _, ok := other.Unsign(tok); ok {
t.Error("a token validated under a different key")
}
}
func TestPreSessionCSRFEnsureAndValid(t *testing.T) {
p, err := httpx.NewPreSessionCSRF([]byte("a-cookie-secret-0123456789abcdef"), false)
if err != nil {
t.Fatalf("NewPreSessionCSRF: %v", err)
}
rec := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/someguid", nil)
tok, err := p.Ensure(rec, r)
if err != nil {
t.Fatalf("Ensure: %v", err)
}
cookies := rec.Result().Cookies()
if len(cookies) != 1 || cookies[0].Name != httpx.PreSessionCookieName {
t.Fatalf("Ensure cookies = %v, want one %s", cookies, httpx.PreSessionCookieName)
}
if !cookies[0].HttpOnly {
t.Error("the pre-session CSRF cookie is not HttpOnly")
}
// Double-submit: cookie value must equal the submitted token.
r2 := httptest.NewRequest("POST", "/someguid?do=auth&action=enroll.begin", nil)
r2.AddCookie(cookies[0])
if !p.Valid(r2, tok) {
t.Error("Valid rejected a matching double-submit token")
}
if p.Valid(r2, tok+"x") {
t.Error("Valid accepted a mismatched token")
}
if p.Valid(httptest.NewRequest("POST", "/", nil), tok) {
t.Error("Valid accepted a token with no cookie")
}
if p.Valid(r2, "") {
t.Error("Valid accepted an empty token")
}
// Ensure reuses a valid cookie rather than rotating it every call.
rec2 := httptest.NewRecorder()
tok2, err := p.Ensure(rec2, r2)
if err != nil {
t.Fatalf("Ensure(reuse): %v", err)
}
if tok2 != tok {
t.Error("Ensure rotated an already-valid token")
}
if len(rec2.Result().Cookies()) != 0 {
t.Error("Ensure re-issued a cookie for a valid token")
}
}