NextMyDay/internal/core/middleware/middleware_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

123 lines
3.4 KiB
Go

package middleware_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"git.lohmar.co.uk/cclohmar/NextMyDay/internal/core/middleware"
)
func TestNewCorrelationID(t *testing.T) {
a := middleware.NewCorrelationID()
b := middleware.NewCorrelationID()
if a == "" || a == b {
t.Fatalf("correlation ids are empty or identical: %q, %q", a, b)
}
if len(a) != 36 || a[8] != '-' || a[13] != '-' || a[18] != '-' || a[23] != '-' {
t.Errorf("correlation id %q is not UUID-shaped", a)
}
}
func TestCorrelationIDMiddleware(t *testing.T) {
var seen string
h := middleware.CorrelationID(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seen = middleware.CorrelationFrom(r.Context())
w.WriteHeader(http.StatusOK)
}))
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("GET", "/home", nil))
header := rec.Header().Get(middleware.CorrelationHeader)
if header == "" {
t.Fatal("response is missing the correlation header")
}
if seen != header {
t.Errorf("context correlation = %q, header = %q; want equal", seen, header)
}
}
func TestCorrelationRoundTrip(t *testing.T) {
if got := middleware.CorrelationFrom(context.Background()); got != "" {
t.Errorf("CorrelationFrom(background) = %q, want empty", got)
}
ctx := middleware.WithCorrelation(context.Background(), "corr-1")
if got := middleware.CorrelationFrom(ctx); got != "corr-1" {
t.Errorf("CorrelationFrom = %q, want corr-1", got)
}
}
func TestRateLimiterAllow(t *testing.T) {
l := middleware.NewRateLimiter(2, time.Minute)
if !l.Allow("k") || !l.Allow("k") {
t.Fatal("the first two requests should be allowed")
}
if l.Allow("k") {
t.Error("the third request should be denied")
}
if !l.Allow("other") {
t.Error("a different key must have its own bucket")
}
}
func TestRateLimiterWindowResets(t *testing.T) {
l := middleware.NewRateLimiter(1, time.Minute)
now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
l.SetClock(func() time.Time { return now })
if !l.Allow("k") || l.Allow("k") {
t.Fatal("limit of 1 not enforced")
}
now = now.Add(2 * time.Minute)
if !l.Allow("k") {
t.Error("the window did not reset")
}
}
func TestRateLimiterMiddleware429(t *testing.T) {
l := middleware.NewRateLimiter(1, time.Minute)
h := l.Middleware(func(*http.Request) string { return "k" },
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }))
rec1 := httptest.NewRecorder()
h.ServeHTTP(rec1, httptest.NewRequest("GET", "/access", nil))
if rec1.Code != http.StatusOK {
t.Fatalf("first request = %d, want 200", rec1.Code)
}
rec2 := httptest.NewRecorder()
h.ServeHTTP(rec2, httptest.NewRequest("GET", "/access", nil))
if rec2.Code != http.StatusTooManyRequests {
t.Fatalf("second request = %d, want 429", rec2.Code)
}
if rec2.Header().Get("Retry-After") == "" {
t.Error("429 is missing a Retry-After header")
}
}
func TestClassAndAuthSensitive(t *testing.T) {
cases := []struct {
path string
want string
}{
{"/", ""},
{"/home", ""},
{"/api/v1", ""},
{"/static/x.css", ""},
{"/access", "access"},
{"/setup", "setup"},
{"/AbCdEf0123456789", "guid"},
{"/AbCdEf0123456789/home", "guid"},
}
for _, tc := range cases {
if got := middleware.Class(tc.path); got != tc.want {
t.Errorf("Class(%q) = %q, want %q", tc.path, got, tc.want)
}
if got := middleware.AuthSensitive(tc.path); got != (tc.want != "") {
t.Errorf("AuthSensitive(%q) = %v, want %v", tc.path, got, tc.want != "")
}
}
}