Files
Crussell/backend/handlers/auth/auth_test.go
T
popertots 44cac94f64 Fix test setup and middleware chain - Handler tests now passing
- Fix TestRequireRoleMiddleware by chaining RequireAuth before RequireRole (role context requirement)
- Remove unused 'strings' import from testdb.go
- Create crussell_test database in Docker setup
- Tests now properly initialize authentication context for role-based tests

Result: handlers test suite passes (13/13 tests)
Remaining failures in admin/auth/bookings/portfolio/scheduling/services/user packages need further investigation (environment setup, database constraints, endpoint initialization)
2026-02-21 23:50:17 +00:00

755 lines
21 KiB
Go

//go:build test
// +build test
package auth
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"crussell/db"
"crussell/internal/dav"
"crussell/testutils/fixtures"
"crussell/testutils/jwt"
"crussell/testutils/testdb"
"github.com/jackc/pgx/v5/pgxpool"
)
// setupTestDB replaces the global db.DB with a test pool and returns a cleanup function
func setupTestDB(t *testing.T) func() {
t.Helper()
pool := testdb.Pool(t)
testdb.Migrate(t, pool)
// Replace global db.DB with test pool
originalDB := db.DB
db.DB = pool
// Initialize JWT for tests
jwt.Init()
// Set up a minimal dav.Service to avoid nil pointer panic
// The real service is only used in a goroutine in RegisterHandler
dav.Service = &dav.BaseService{}
return func() {
db.DB = originalDB
pool.Close()
}
}
// helper function to make JSON request
func makeRequest(handler http.Handler, method, path string, body interface{}) *httptest.ResponseRecorder {
var req *http.Request
if body != nil {
bodyBytes, _ := json.Marshal(body)
req = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
} else {
req = httptest.NewRequest(method, path, nil)
}
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
return w
}
// Helper to parse response body
func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
return json.Unmarshal(w.Body.Bytes(), dest)
}
// =============================================================================
// Register Handler Tests
// =============================================================================
func TestRegister_Success(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(RegisterHandler)
body := RegisterRequest{
FirstName: "John",
LastName: "Doe",
Email: "john.doe@test.com",
Password: "password123",
Phone: "07700900000",
DateOfBirth: "1990-01-15",
AgreedToPolicy: true,
}
w := makeRequest(handler, "POST", "/api/register", body)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
}
// Verify user was created in DB
var userID string
err := db.DB.QueryRow(context.Background(),
"SELECT id FROM users WHERE email = $1", "john.doe@test.com").Scan(&userID)
if err != nil {
t.Errorf("failed to find user in DB: %v", err)
}
// Clean up
db.DB.Exec(context.Background(), "DELETE FROM users WHERE id = $1", userID)
}
func TestRegister_InvalidInput_MissingFields(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(RegisterHandler)
tests := []struct {
name string
body RegisterRequest
}{
{
name: "missing firstName",
body: RegisterRequest{LastName: "Doe", Email: "test@test.com", Password: "pass", Phone: "07700900000", DateOfBirth: "1990-01-15", AgreedToPolicy: true},
},
{
name: "missing lastName",
body: RegisterRequest{FirstName: "John", Email: "test@test.com", Password: "pass", Phone: "07700900000", DateOfBirth: "1990-01-15", AgreedToPolicy: true},
},
{
name: "missing email",
body: RegisterRequest{FirstName: "John", LastName: "Doe", Password: "pass", Phone: "07700900000", DateOfBirth: "1990-01-15", AgreedToPolicy: true},
},
{
name: "missing phone",
body: RegisterRequest{FirstName: "John", LastName: "Doe", Email: "test@test.com", Password: "pass", DateOfBirth: "1990-01-15", AgreedToPolicy: true},
},
{
name: "missing dateOfBirth",
body: RegisterRequest{FirstName: "John", LastName: "Doe", Email: "test@test.com", Password: "pass", Phone: "07700900000", AgreedToPolicy: true},
},
{
name: "did not agree to policy",
body: RegisterRequest{FirstName: "John", LastName: "Doe", Email: "test@test.com", Password: "pass", Phone: "07700900000", DateOfBirth: "1990-01-15", AgreedToPolicy: false},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := makeRequest(handler, "POST", "/api/register", tt.body)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d", w.Code)
}
})
}
}
func TestRegister_InvalidInput_InvalidEmail(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(RegisterHandler)
body := RegisterRequest{
FirstName: "John",
LastName: "Doe",
Email: "not-an-email",
Password: "password123",
Phone: "07700900000",
DateOfBirth: "1990-01-15",
AgreedToPolicy: true,
}
w := makeRequest(handler, "POST", "/api/register", body)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
}
}
func TestRegister_InvalidInput_InvalidPhone(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(RegisterHandler)
body := RegisterRequest{
FirstName: "John",
LastName: "Doe",
Email: "john@test.com",
Password: "password123",
Phone: "12345", // Not a valid UK phone
DateOfBirth: "1990-01-15",
AgreedToPolicy: true,
}
w := makeRequest(handler, "POST", "/api/register", body)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
}
}
func TestRegister_InvalidInput_Under16(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(RegisterHandler)
// Calculate a date that makes them under 16
under16DOB := time.Now().AddDate(-15, 0, 0).Format("2006-01-02")
body := RegisterRequest{
FirstName: "Young",
LastName: "User",
Email: "young@test.com",
Password: "password123",
Phone: "07700900000",
DateOfBirth: under16DOB,
AgreedToPolicy: true,
}
w := makeRequest(handler, "POST", "/api/register", body)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
}
}
func TestRegister_DuplicateEmail(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(RegisterHandler)
// First create a user
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
// Now try to register with same email
body := RegisterRequest{
FirstName: "John",
LastName: "Doe",
Email: "user@test.com", // Same as fixture
Password: "password123",
Phone: "07700900000",
DateOfBirth: "1990-01-15",
AgreedToPolicy: true,
}
w := makeRequest(handler, "POST", "/api/register", body)
if w.Code != http.StatusConflict {
t.Errorf("expected status 409, got %d. body: %s", w.Code, w.Body.String())
}
}
// =============================================================================
// Login Handler Tests
// =============================================================================
func TestLogin_Success(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(LoginHandler)
// Create a test user
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
body := LoginRequest{
Email: "user@test.com",
Password: "testpassword123",
}
w := makeRequest(handler, "POST", "/api/login", body)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var resp struct {
Token string `json:"token"`
}
if err := parseResponseBody(w, &resp); err != nil {
t.Errorf("failed to parse response: %v", err)
}
if resp.Token == "" {
t.Error("expected token in response, got empty string")
}
}
func TestLogin_InvalidCredentials_WrongPassword(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(LoginHandler)
// Create a test user
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
body := LoginRequest{
Email: "user@test.com",
Password: "wrongpassword",
}
w := makeRequest(handler, "POST", "/api/login", body)
if w.Code != http.StatusUnauthorized {
t.Errorf("expected status 401, got %d. body: %s", w.Code, w.Body.String())
}
}
func TestLogin_InvalidCredentials_NonExistentEmail(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(LoginHandler)
body := LoginRequest{
Email: "nonexistent@test.com",
Password: "password123",
}
w := makeRequest(handler, "POST", "/api/login", body)
if w.Code != http.StatusUnauthorized {
t.Errorf("expected status 401, got %d. body: %s", w.Code, w.Body.String())
}
}
// =============================================================================
// Refresh Token Handler Tests
// =============================================================================
func TestRefreshToken_Success(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(RefreshTokenHandler)
// Create a test user
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
// Generate a valid token
token := jwt.GenerateTestToken(userID, "verified_email")
req := httptest.NewRequest("POST", "/api/refresh-token", nil)
req.Header.Set("Authorization", "Bearer "+token)
w := httptest.NewRecorder()
// Use the middleware to set up context
ctx := req.Context()
ctx = context.WithValue(ctx, "user_id", userID)
ctx = context.WithValue(ctx, "user_role", "verified_email")
req = req.WithContext(ctx)
handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var resp struct {
Token string `json:"token"`
}
if err := parseResponseBody(w, &resp); err != nil {
t.Errorf("failed to parse response: %v", err)
}
if resp.Token == "" {
t.Error("expected new token in response, got empty string")
}
}
func TestRefreshToken_Unauthorized_NoToken(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(RefreshTokenHandler)
req := httptest.NewRequest("POST", "/api/refresh-token", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
// Without proper auth middleware, userID/role won't be in context
// The handler tries to query DB with empty userID, which should fail
if w.Code != http.StatusUnauthorized && w.Code != http.StatusInternalServerError {
t.Errorf("expected status 401 or 500, got %d. body: %s", w.Code, w.Body.String())
}
}
// =============================================================================
// Verify Generate Handler Tests
// =============================================================================
func TestVerifyGenerate_ValidEmail(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(GenerateVerificationCodeHandler)
// Create a test user
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
body := VerificationCodeRequest{
Email: "user@test.com",
}
w := makeRequest(handler, "POST", "/api/verify/generate", body)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var resp VerificationResponse
if err := parseResponseBody(w, &resp); err != nil {
t.Errorf("failed to parse response: %v", err)
}
if !resp.Success {
t.Error("expected success=true in response")
}
// Verify a code was created in DB
var codeID string
err = db.DB.QueryRow(context.Background(),
"SELECT id FROM verification_codes WHERE user_id = $1", userID).Scan(&codeID)
if err != nil {
t.Errorf("failed to find verification code in DB: %v", err)
}
// Clean up
db.DB.Exec(context.Background(), "DELETE FROM verification_codes WHERE user_id = $1", userID)
}
func TestVerifyGenerate_NonExistentEmail(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(GenerateVerificationCodeHandler)
// Security: should return success even if email doesn't exist
body := VerificationCodeRequest{
Email: "nonexistent@test.com",
}
w := makeRequest(handler, "POST", "/api/verify/generate", body)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var resp VerificationResponse
if err := parseResponseBody(w, &resp); err != nil {
t.Errorf("failed to parse response: %v", err)
}
// Should return success for security (don't reveal if email exists)
if !resp.Success {
t.Error("expected success=true in response for non-existent email")
}
}
// =============================================================================
// Verify Check Handler Tests
// =============================================================================
func TestVerifyCheck_ValidCode(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(VerifyCodeHandler)
// Create a test user
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
// Create a verification code
var code string
expiresAt := time.Now().Add(24 * time.Hour)
err = db.DB.QueryRow(context.Background(),
`INSERT INTO verification_codes (user_id, purpose, expires_at) VALUES ($1, 'email_verify', $2) RETURNING code`,
userID, expiresAt).Scan(&code)
if err != nil {
t.Fatalf("failed to create verification code: %v", err)
}
defer db.DB.Exec(context.Background(), "DELETE FROM verification_codes WHERE user_id = $1", userID)
body := VerifyCodeRequest{
Code: code,
}
w := makeRequest(handler, "POST", "/api/verify/check", body)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var resp VerificationResponse
if err := parseResponseBody(w, &resp); err != nil {
t.Errorf("failed to parse response: %v", err)
}
if !resp.Success {
t.Error("expected success=true in response")
}
// Verify code is marked as used
var usedAt *time.Time
err = db.DB.QueryRow(context.Background(),
"SELECT used_at FROM verification_codes WHERE code = $1", code).Scan(&usedAt)
if err != nil || usedAt == nil {
t.Error("expected verification code to be marked as used")
}
}
func TestVerifyCheck_InvalidCode(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(VerifyCodeHandler)
body := VerifyCodeRequest{
Code: "nonexistent-code-12345",
}
w := makeRequest(handler, "POST", "/api/verify/check", body)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
}
}
func TestVerifyCheck_ExpiredCode(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(VerifyCodeHandler)
// Create a test user
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
// Create an expired verification code
var code string
expiresAt := time.Now().Add(-1 * time.Hour) // Expired 1 hour ago
err = db.DB.QueryRow(context.Background(),
`INSERT INTO verification_codes (user_id, purpose, expires_at) VALUES ($1, 'email_verify', $2) RETURNING code`,
userID, expiresAt).Scan(&code)
if err != nil {
t.Fatalf("failed to create verification code: %v", err)
}
defer db.DB.Exec(context.Background(), "DELETE FROM verification_codes WHERE user_id = $1", userID)
body := VerifyCodeRequest{
Code: code,
}
w := makeRequest(handler, "POST", "/api/verify/check", body)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
}
}
// =============================================================================
// Additional Edge Case Tests
// =============================================================================
func TestLogin_InvalidRequest(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(LoginHandler)
// Send invalid JSON
req := httptest.NewRequest("POST", "/api/login", bytes.NewReader([]byte("not json")))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d", w.Code)
}
}
func TestRegister_NameTooLong(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(RegisterHandler)
// First name > 50 chars
longName := string(bytes.Repeat([]byte("a"), 51))
body := RegisterRequest{
FirstName: longName,
LastName: "Doe",
Email: "john@test.com",
Password: "password123",
Phone: "07700900000",
DateOfBirth: "1990-01-15",
AgreedToPolicy: true,
}
w := makeRequest(handler, "POST", "/api/register", body)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
}
}
func TestRegister_InvalidNameCharacters(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(RegisterHandler)
// Name with numbers (invalid)
body := RegisterRequest{
FirstName: "John123",
LastName: "Doe",
Email: "john@test.com",
Password: "password123",
Phone: "07700900000",
DateOfBirth: "1990-01-15",
AgreedToPolicy: true,
}
w := makeRequest(handler, "POST", "/api/register", body)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
}
}
func TestVerifyCheck_AlreadyUsed(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(VerifyCodeHandler)
// Create a test user
userID, err := fixtures.CreateTestUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
// Create a verification code
var code string
expiresAt := time.Now().Add(24 * time.Hour)
err = db.DB.QueryRow(context.Background(),
`INSERT INTO verification_codes (user_id, purpose, expires_at) VALUES ($1, 'email_verify', $2) RETURNING code`,
userID, expiresAt).Scan(&code)
if err != nil {
t.Fatalf("failed to create verification code: %v", err)
}
defer db.DB.Exec(context.Background(), "DELETE FROM verification_codes WHERE user_id = $1", userID)
// First verification should succeed
body := VerifyCodeRequest{
Code: code,
}
w := makeRequest(handler, "POST", "/api/verify/check", body)
if w.Code != http.StatusOK {
t.Errorf("first verification: expected status 200, got %d", w.Code)
}
// Second verification with same code should return 403 (already used)
w = makeRequest(handler, "POST", "/api/verify/check", body)
if w.Code != http.StatusForbidden {
t.Errorf("second verification: expected status 403, got %d", w.Code)
}
}
func TestVerifyCheck_RoleChangeToVerified(t *testing.T) {
cleanup := setupTestDB(t)
defer cleanup()
handler := http.HandlerFunc(VerifyCodeHandler)
// Create an unverified user
userID, err := fixtures.CreateTestUnverifiedUser(db.DB)
if err != nil {
t.Fatalf("failed to create test user: %v", err)
}
defer fixtures.DeleteUser(db.DB, userID)
// Verify initial role is unverified_email
var initialRole string
err = db.DB.QueryRow(context.Background(),
"SELECT account_role FROM users WHERE id = $1", userID).Scan(&initialRole)
if err != nil {
t.Fatalf("failed to check initial role: %v", err)
}
if initialRole != "unverified_email" {
t.Errorf("expected initial role 'unverified_email', got %s", initialRole)
}
// Create a verification code
var code string
expiresAt := time.Now().Add(24 * time.Hour)
err = db.DB.QueryRow(context.Background(),
`INSERT INTO verification_codes (user_id, purpose, expires_at) VALUES ($1, 'email_verify', $2) RETURNING code`,
userID, expiresAt).Scan(&code)
if err != nil {
t.Fatalf("failed to create verification code: %v", err)
}
defer db.DB.Exec(context.Background(), "DELETE FROM verification_codes WHERE user_id = $1", userID)
// Verify the code
body := VerifyCodeRequest{
Code: code,
}
w := makeRequest(handler, "POST", "/api/verify/check", body)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
// Check that user's role changed to verified_email
var newRole string
err = db.DB.QueryRow(context.Background(),
"SELECT account_role FROM users WHERE id = $1", userID).Scan(&newRole)
if err != nil {
t.Errorf("failed to check new role: %v", err)
}
if newRole != "verified_email" {
t.Errorf("expected role to change to 'verified_email', got %s", newRole)
}
}
// Ensure test compilation - import pgxpool to avoid unused import
var _ = func() *pgxpool.Pool { return nil }