Files
Crussell/backend/handlers/handlers_test.go
T

198 lines
6.0 KiB
Go

//go:build test
// +build test
package handlers
// Package handlers contains tests for core middleware and health checks.
//
// Test Coverage:
// - Health check: Basic HTTP 200 response
// - RequireAuth middleware: Blocks unauthenticated requests (401), allows valid JWT (200)
// - RequireRole middleware: Blocks non-admin users (403), allows admins (200)
// - Integration test: Full user flow with JWT auth and context propagation
//
// Note: These tests focus on middleware behavior, not specific handler business logic.
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"crussell/mw"
"crussell/testutils/fixtures"
"crussell/testutils/jwt"
"crussell/testutils/testdb"
)
// TestHealthCheck verifies the health check endpoint returns HTTP 200 OK.
// This test ensures the basic HTTP server is responding and the health
// check handler is properly wired up to return a status response.
func TestHealthCheck(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
})
server := httptest.NewServer(handler)
defer server.Close()
resp, err := server.Client().Get(server.URL)
if err != nil {
t.Fatalf("failed to make request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %d", resp.StatusCode)
}
}
// TestRequireAuthMiddleware verifies the JWT authentication middleware correctly
// blocks unauthenticated requests and allows valid JWT tokens through.
// It tests three scenarios: missing auth header (401), valid token (200 with
// user context), and invalid token (401).
func TestRequireAuthMiddleware(t *testing.T) {
handler := mw.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID, _ := r.Context().Value(mw.UserIDKey).(string)
role, _ := r.Context().Value(mw.UserRoleKey).(string)
w.Write([]byte(`{"user_id":"` + userID + `","role":"` + role + `"}`))
}))
t.Run("no auth header returns 401", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusUnauthorized {
t.Errorf("expected status 401, got %d", w.Code)
}
})
t.Run("valid token passes auth", func(t *testing.T) {
jwt.Init()
token := jwt.GenerateTestToken("test-user-123", "verified_email")
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Authorization", "Bearer "+token)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
body, _ := io.ReadAll(w.Body)
var resp map[string]string
json.Unmarshal(body, &resp)
if resp["user_id"] != "test-user-123" {
t.Errorf("expected user_id test-user-123, got %s", resp["user_id"])
}
if resp["role"] != "verified_email" {
t.Errorf("expected role verified_email, got %s", resp["role"])
}
})
t.Run("invalid token returns 401", func(t *testing.T) {
jwt.Init()
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Authorization", "Bearer invalid-token")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusUnauthorized {
t.Errorf("expected status 401, got %d", w.Code)
}
})
}
// TestRequireRoleMiddleware verifies the role-based access control middleware
// correctly blocks non-admin users (403) and allows admin users (200) to access
// protected resources. It chains RequireAuth before RequireRole to populate
// the role in the request context.
func TestRequireRoleMiddleware(t *testing.T) {
// Chain RequireAuth before RequireRole to set the role in context
// RequireRole expects role to be in context, but that's only set by RequireAuth
adminOnlyHandler := mw.RequireAuth(mw.RequireRole("admin")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"success":true}`))
})))
t.Run("admin role passes", func(t *testing.T) {
jwt.Init()
token := jwt.GenerateAdminToken()
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Authorization", "Bearer "+token)
w := httptest.NewRecorder()
adminOnlyHandler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
})
t.Run("non-admin role returns 403", func(t *testing.T) {
jwt.Init()
token := jwt.GenerateUserToken("test-user")
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Authorization", "Bearer "+token)
w := httptest.NewRecorder()
adminOnlyHandler.ServeHTTP(w, req)
if w.Code != http.StatusForbidden {
t.Errorf("expected status 403, got %d", w.Code)
}
})
}
// TestIntegration_UserFlow tests the full authentication flow end-to-end,
// verifying that JWT tokens are properly validated, user ID is extracted from
// the token, and context values are correctly propagated to handlers.
// This is an integration test that validates the complete middleware chain.
func TestIntegration_UserFlow(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
pool := testdb.Pool(t)
defer pool.Close()
userID, err := fixtures.CreateTestUser(pool)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(pool, userID)
jwt.Init()
token := jwt.GenerateUserToken(userID)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "no auth", http.StatusUnauthorized)
return
}
userIDCtx, ok := r.Context().Value(mw.UserIDKey).(string)
if !ok {
http.Error(w, "no user id in context", http.StatusInternalServerError)
return
}
w.Write([]byte(`{"user_id":"` + userIDCtx + `"}`))
})
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Authorization", "Bearer "+token)
w := httptest.NewRecorder()
mw.RequireAuth(handler).ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
}