- Add TestMain to all 10 test packages (schema DROP+CREATE runs once per package) - Convert per-test setupTestDB to resetTestData (TRUNCATE only, ~60% faster) - Add 3 missing tables to TruncateTables (booking_edit_requests, exceptional_group_applications, business_settings) - Remove dead truncateDiscountTables helper - Consolidate discount_test.go into package bookings (was external test package) - Update testutils.SetupTestDB to truncate-only - Fix unused imports across user, bookings, and handlers packages - Verify: 286 passing, 2 skipped, 0 failures with -count=2 (no state leakage)
209 lines
6.1 KiB
Go
209 lines
6.1 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"
|
|
"os"
|
|
"testing"
|
|
|
|
"crussell/db"
|
|
"crussell/mw"
|
|
"crussell/testutils/fixtures"
|
|
"crussell/testutils/jwt"
|
|
"crussell/testutils/testdb"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
pool, err := testdb.NewPool("")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
testdb.Migrate(&testing.T{}, pool)
|
|
db.DB = pool
|
|
jwt.Init()
|
|
code := m.Run()
|
|
pool.Close()
|
|
os.Exit(code)
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
|
|
userID, err := fixtures.CreateTestUser(db.DB)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
defer fixtures.DeleteUser(db.DB, userID)
|
|
|
|
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)
|
|
}
|
|
}
|