//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 }