2025-12-25 00:35:38 +00:00
//go:build integration
// +build integration
package handlers
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"veza-backend-api/internal/core/auth"
"veza-backend-api/internal/database"
"veza-backend-api/internal/dto"
apperrors "veza-backend-api/internal/errors"
"veza-backend-api/internal/models"
"veza-backend-api/internal/repositories"
"veza-backend-api/internal/services"
"veza-backend-api/internal/validators"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zaptest"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// setupAuthIntegrationTestRouter creates a test router with all auth services for integration testing
func setupAuthIntegrationTestRouter ( t * testing . T ) ( * gin . Engine , * auth . AuthService , * services . SessionService , * services . TwoFactorService , * services . UserService , * gorm . DB , func ( ) ) {
gin . SetMode ( gin . TestMode )
logger := zaptest . NewLogger ( t )
// Setup in-memory database
db , err := gorm . Open ( sqlite . Open ( ":memory:" ) , & gorm . Config { } )
require . NoError ( t , err )
// Enable foreign keys for SQLite
db . Exec ( "PRAGMA foreign_keys = ON" )
// Auto-migrate all models needed for auth flow
err = db . AutoMigrate (
& models . User { } ,
& models . RefreshToken { } ,
& models . Session { } ,
& models . Role { } ,
& models . Permission { } ,
& models . UserRole { } ,
& models . RolePermission { } ,
)
require . NoError ( t , err )
// Create email_verification_tokens table manually (no GORM model)
err = db . Exec ( `
CREATE TABLE IF NOT EXISTS email_verification_tokens (
id TEXT PRIMARY KEY ,
user_id TEXT NOT NULL REFERENCES users ( id ) ON DELETE CASCADE ,
token TEXT NOT NULL UNIQUE ,
token_hash TEXT NOT NULL ,
email TEXT NOT NULL ,
verified INTEGER NOT NULL DEFAULT 0 ,
used INTEGER NOT NULL DEFAULT 0 ,
verified_at TIMESTAMP ,
expires_at TIMESTAMP NOT NULL ,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)
` ) . Error
require . NoError ( t , err )
// Create database wrapper
dbWrapper := & database . Database { }
dbWrapper . GormDB = db
// Setup services
emailValidator := validators . NewEmailValidator ( db )
passwordValidator := validators . NewPasswordValidator ( )
passwordService := services . NewPasswordService ( dbWrapper , logger )
2026-03-05 18:22:31 +00:00
jwtService , err := services . NewJWTService ( "" , "" , "test-secret-key-must-be-32-chars-long" , "test-issuer" , "test-audience" )
2025-12-25 00:35:38 +00:00
require . NoError ( t , err )
refreshTokenService := services . NewRefreshTokenService ( db )
emailVerificationService := services . NewEmailVerificationService ( dbWrapper , logger )
passwordResetService := services . NewPasswordResetService ( dbWrapper , logger )
emailService := services . NewEmailService ( dbWrapper , logger )
// Create AuthService
authService := auth . NewAuthService (
db ,
emailValidator ,
passwordValidator ,
passwordService ,
jwtService ,
refreshTokenService ,
emailVerificationService ,
passwordResetService ,
emailService ,
nil , // jobWorker - not needed for integration tests
2026-02-14 17:29:37 +00:00
nil , // refreshLock - not needed for integration tests
2025-12-25 00:35:38 +00:00
logger ,
)
// Create other services
sessionService := services . NewSessionService ( dbWrapper , logger )
twoFactorService := services . NewTwoFactorService ( dbWrapper , logger )
// Create UserService with GORM repository
userRepo := repositories . NewGormUserRepository ( db )
userService := services . NewUserServiceWithDB ( userRepo , db )
// Setup router with all auth endpoints
router := gin . New ( )
authGroup := router . Group ( "/auth" )
{
authGroup . POST ( "/login" , Login ( authService , sessionService , twoFactorService , logger ) )
authGroup . POST ( "/register" , Register ( authService , logger ) )
authGroup . POST ( "/refresh" , Refresh ( authService , logger ) )
authGroup . POST ( "/logout" , Logout ( authService , sessionService , logger ) )
authGroup . POST ( "/verify-email" , VerifyEmail ( authService ) )
authGroup . POST ( "/resend-verification" , ResendVerification ( authService , logger ) )
authGroup . GET ( "/check-username" , CheckUsername ( authService ) )
2026-01-13 18:47:57 +00:00
2025-12-25 00:35:38 +00:00
// Protected route for /me
protected := authGroup . Group ( "" )
protected . Use ( func ( c * gin . Context ) {
// Mock auth middleware - extract token from Authorization header
authHeader := c . GetHeader ( "Authorization" )
if authHeader == "" {
RespondWithAppError ( c , apperrors . NewUnauthorizedError ( "Unauthorized" ) )
c . Abort ( )
return
}
2026-01-13 18:47:57 +00:00
2025-12-25 00:35:38 +00:00
// Extract token (simplified - in real app, would validate JWT)
if ! strings . HasPrefix ( authHeader , "Bearer " ) {
RespondWithAppError ( c , apperrors . NewUnauthorizedError ( "Invalid authorization header" ) )
c . Abort ( )
return
}
2026-01-13 18:47:57 +00:00
2025-12-25 00:35:38 +00:00
token := strings . TrimPrefix ( authHeader , "Bearer " )
2026-01-13 18:47:57 +00:00
2025-12-25 00:35:38 +00:00
// Validate token and extract user ID
claims , err := authService . JWTService . ValidateToken ( token )
if err != nil {
RespondWithAppError ( c , apperrors . NewUnauthorizedError ( "Invalid token" ) )
c . Abort ( )
return
}
2026-01-13 18:47:57 +00:00
2025-12-25 00:35:38 +00:00
// Extract user ID from claims
uid := claims . UserID
if uid == uuid . Nil {
RespondWithAppError ( c , apperrors . NewUnauthorizedError ( "Invalid token claims" ) )
c . Abort ( )
return
}
2026-01-13 18:47:57 +00:00
2025-12-25 00:35:38 +00:00
c . Set ( "user_id" , uid )
c . Next ( )
} )
protected . GET ( "/me" , GetMe ( userService ) )
}
cleanup := func ( ) {
// Database cleanup handled by test
}
return router , authService , sessionService , twoFactorService , userService , db , cleanup
}
// TestAuthFlow_CompleteFlow tests the complete authentication flow: Register -> Login -> Refresh -> Logout
func TestAuthFlow_CompleteFlow ( t * testing . T ) {
router , _ , _ , _ , _ , db , cleanup := setupAuthIntegrationTestRouter ( t )
defer cleanup ( )
// Step 1: Register a new user
registerReq := dto . RegisterRequest {
Email : "test@example.com" ,
Username : "testuser" ,
2026-01-13 18:47:57 +00:00
Password : "SecurePassword123!" ,
2025-12-25 00:35:38 +00:00
PasswordConfirm : "SecurePassword123!" ,
}
registerBody , _ := json . Marshal ( registerReq )
registerHTTPReq := httptest . NewRequest ( http . MethodPost , "/auth/register" , bytes . NewBuffer ( registerBody ) )
registerHTTPReq . Header . Set ( "Content-Type" , "application/json" )
registerW := httptest . NewRecorder ( )
router . ServeHTTP ( registerW , registerHTTPReq )
assert . Equal ( t , http . StatusCreated , registerW . Code )
var registerResponse APIResponse
err := json . Unmarshal ( registerW . Body . Bytes ( ) , & registerResponse )
require . NoError ( t , err )
assert . True ( t , registerResponse . Success )
// Get user from database to verify email
var user models . User
err = db . Where ( "email = ?" , "test@example.com" ) . First ( & user ) . Error
require . NoError ( t , err )
// Step 2: Verify email (simulate email verification)
// Get verification token from database (using raw SQL since there's no GORM model)
var token string
err = db . Raw ( "SELECT token FROM email_verification_tokens WHERE user_id = ? AND used = 0 ORDER BY created_at DESC LIMIT 1" , user . ID . String ( ) ) . Scan ( & token ) . Error
if err != nil {
2026-02-22 16:53:00 +00:00
// INT-04: Skip - email verification token not found (integration setup/data race).
t . Skip ( "email verification token not found in database" )
2025-12-25 00:35:38 +00:00
return
}
verifyReq := httptest . NewRequest ( http . MethodPost , "/auth/verify-email?token=" + token , nil )
verifyW := httptest . NewRecorder ( )
router . ServeHTTP ( verifyW , verifyReq )
assert . Equal ( t , http . StatusOK , verifyW . Code )
// Step 3: Login
loginReq := dto . LoginRequest {
Email : "test@example.com" ,
Password : "SecurePassword123!" ,
RememberMe : false ,
}
loginBody , _ := json . Marshal ( loginReq )
loginHTTPReq := httptest . NewRequest ( http . MethodPost , "/auth/login" , bytes . NewBuffer ( loginBody ) )
loginHTTPReq . Header . Set ( "Content-Type" , "application/json" )
loginW := httptest . NewRecorder ( )
router . ServeHTTP ( loginW , loginHTTPReq )
assert . Equal ( t , http . StatusOK , loginW . Code )
var loginResponse APIResponse
err = json . Unmarshal ( loginW . Body . Bytes ( ) , & loginResponse )
require . NoError ( t , err )
assert . True ( t , loginResponse . Success )
// Extract tokens from response
loginDataBytes , _ := json . Marshal ( loginResponse . Data )
var loginData dto . LoginResponse
err = json . Unmarshal ( loginDataBytes , & loginData )
require . NoError ( t , err )
assert . NotEmpty ( t , loginData . Token . AccessToken )
assert . NotEmpty ( t , loginData . Token . RefreshToken )
// Step 4: Use access token to access protected endpoint
meReq := httptest . NewRequest ( http . MethodGet , "/auth/me" , nil )
meReq . Header . Set ( "Authorization" , "Bearer " + loginData . Token . AccessToken )
meW := httptest . NewRecorder ( )
router . ServeHTTP ( meW , meReq )
assert . Equal ( t , http . StatusOK , meW . Code )
var meResponse APIResponse
err = json . Unmarshal ( meW . Body . Bytes ( ) , & meResponse )
require . NoError ( t , err )
assert . True ( t , meResponse . Success )
// Step 5: Refresh token
refreshReq := dto . RefreshRequest {
RefreshToken : loginData . Token . RefreshToken ,
}
refreshBody , _ := json . Marshal ( refreshReq )
refreshHTTPReq := httptest . NewRequest ( http . MethodPost , "/auth/refresh" , bytes . NewBuffer ( refreshBody ) )
refreshHTTPReq . Header . Set ( "Content-Type" , "application/json" )
refreshW := httptest . NewRecorder ( )
router . ServeHTTP ( refreshW , refreshHTTPReq )
assert . Equal ( t , http . StatusOK , refreshW . Code )
var refreshResponse APIResponse
err = json . Unmarshal ( refreshW . Body . Bytes ( ) , & refreshResponse )
require . NoError ( t , err )
assert . True ( t , refreshResponse . Success )
// Extract new tokens
refreshDataBytes , _ := json . Marshal ( refreshResponse . Data )
var refreshData dto . TokenResponse
err = json . Unmarshal ( refreshDataBytes , & refreshData )
require . NoError ( t , err )
assert . NotEmpty ( t , refreshData . AccessToken )
assert . NotEmpty ( t , refreshData . RefreshToken )
// Step 6: Logout
logoutReqBody := map [ string ] string {
"refresh_token" : refreshData . RefreshToken ,
}
logoutBody , _ := json . Marshal ( logoutReqBody )
logoutHTTPReq := httptest . NewRequest ( http . MethodPost , "/auth/logout" , bytes . NewBuffer ( logoutBody ) )
logoutHTTPReq . Header . Set ( "Content-Type" , "application/json" )
logoutHTTPReq . Header . Set ( "Authorization" , "Bearer " + refreshData . AccessToken )
logoutW := httptest . NewRecorder ( )
router . ServeHTTP ( logoutW , logoutHTTPReq )
assert . Equal ( t , http . StatusOK , logoutW . Code )
var logoutResponse APIResponse
err = json . Unmarshal ( logoutW . Body . Bytes ( ) , & logoutResponse )
require . NoError ( t , err )
assert . True ( t , logoutResponse . Success )
}
// TestAuthFlow_EmailVerificationFlow tests the email verification flow: Register -> Login fails -> Verify -> Login succeeds
func TestAuthFlow_EmailVerificationFlow ( t * testing . T ) {
router , _ , _ , _ , _ , db , cleanup := setupAuthIntegrationTestRouter ( t )
defer cleanup ( )
// Step 1: Register a new user
registerReq := dto . RegisterRequest {
Email : "test2@example.com" ,
Username : "testuser2" ,
2026-01-13 18:47:57 +00:00
Password : "SecurePassword123!" ,
2025-12-25 00:35:38 +00:00
PasswordConfirm : "SecurePassword123!" ,
}
registerBody , _ := json . Marshal ( registerReq )
registerHTTPReq := httptest . NewRequest ( http . MethodPost , "/auth/register" , bytes . NewBuffer ( registerBody ) )
registerHTTPReq . Header . Set ( "Content-Type" , "application/json" )
registerW := httptest . NewRecorder ( )
router . ServeHTTP ( registerW , registerHTTPReq )
assert . Equal ( t , http . StatusCreated , registerW . Code )
// Step 2: Try to login before email verification (should fail)
loginReq := dto . LoginRequest {
Email : "test2@example.com" ,
Password : "SecurePassword123!" ,
RememberMe : false ,
}
loginBody , _ := json . Marshal ( loginReq )
loginHTTPReq := httptest . NewRequest ( http . MethodPost , "/auth/login" , bytes . NewBuffer ( loginBody ) )
loginHTTPReq . Header . Set ( "Content-Type" , "application/json" )
loginW := httptest . NewRecorder ( )
router . ServeHTTP ( loginW , loginHTTPReq )
assert . Equal ( t , http . StatusForbidden , loginW . Code ) // Email not verified
// Step 3: Get verification token and verify email
var user models . User
err := db . Where ( "email = ?" , "test2@example.com" ) . First ( & user ) . Error
require . NoError ( t , err )
// Get verification token from database
var token string
err = db . Raw ( "SELECT token FROM email_verification_tokens WHERE user_id = ? AND used = 0 ORDER BY created_at DESC LIMIT 1" , user . ID . String ( ) ) . Scan ( & token ) . Error
if err != nil {
2026-02-22 16:53:00 +00:00
// INT-04: Skip - email verification token not found (integration setup/data race).
t . Skip ( "email verification token not found in database" )
2025-12-25 00:35:38 +00:00
return
}
verifyReq := httptest . NewRequest ( http . MethodPost , "/auth/verify-email?token=" + token , nil )
verifyW := httptest . NewRecorder ( )
router . ServeHTTP ( verifyW , verifyReq )
assert . Equal ( t , http . StatusOK , verifyW . Code )
// Step 4: Try to login after email verification (should succeed)
loginHTTPReq2 := httptest . NewRequest ( http . MethodPost , "/auth/login" , bytes . NewBuffer ( loginBody ) )
loginHTTPReq2 . Header . Set ( "Content-Type" , "application/json" )
loginW2 := httptest . NewRecorder ( )
router . ServeHTTP ( loginW2 , loginHTTPReq2 )
assert . Equal ( t , http . StatusOK , loginW2 . Code )
var loginResponse APIResponse
err = json . Unmarshal ( loginW2 . Body . Bytes ( ) , & loginResponse )
require . NoError ( t , err )
assert . True ( t , loginResponse . Success )
}
// TestAuthFlow_CheckUsername tests username availability checking
func TestAuthFlow_CheckUsername ( t * testing . T ) {
router , _ , _ , _ , _ , _ , cleanup := setupAuthIntegrationTestRouter ( t )
defer cleanup ( )
// Step 1: Register a user
registerReq := dto . RegisterRequest {
Email : "test3@example.com" ,
Username : "testuser3" ,
2026-01-13 18:47:57 +00:00
Password : "SecurePassword123!" ,
2025-12-25 00:35:38 +00:00
PasswordConfirm : "SecurePassword123!" ,
}
registerBody , _ := json . Marshal ( registerReq )
registerHTTPReq := httptest . NewRequest ( http . MethodPost , "/auth/register" , bytes . NewBuffer ( registerBody ) )
registerHTTPReq . Header . Set ( "Content-Type" , "application/json" )
registerW := httptest . NewRecorder ( )
router . ServeHTTP ( registerW , registerHTTPReq )
assert . Equal ( t , http . StatusCreated , registerW . Code )
// Step 2: Check if username is available (should be false - already taken)
checkReq := httptest . NewRequest ( http . MethodGet , "/auth/check-username?username=testuser3" , nil )
checkW := httptest . NewRecorder ( )
router . ServeHTTP ( checkW , checkReq )
assert . Equal ( t , http . StatusOK , checkW . Code )
var checkResponse APIResponse
err := json . Unmarshal ( checkW . Body . Bytes ( ) , & checkResponse )
require . NoError ( t , err )
assert . True ( t , checkResponse . Success )
checkDataBytes , _ := json . Marshal ( checkResponse . Data )
var checkData map [ string ] interface { }
err = json . Unmarshal ( checkDataBytes , & checkData )
require . NoError ( t , err )
assert . Equal ( t , false , checkData [ "available" ] )
// Step 3: Check if different username is available (should be true)
checkReq2 := httptest . NewRequest ( http . MethodGet , "/auth/check-username?username=newuser" , nil )
checkW2 := httptest . NewRecorder ( )
router . ServeHTTP ( checkW2 , checkReq2 )
assert . Equal ( t , http . StatusOK , checkW2 . Code )
var checkResponse2 APIResponse
err = json . Unmarshal ( checkW2 . Body . Bytes ( ) , & checkResponse2 )
require . NoError ( t , err )
assert . True ( t , checkResponse2 . Success )
checkDataBytes2 , _ := json . Marshal ( checkResponse2 . Data )
var checkData2 map [ string ] interface { }
err = json . Unmarshal ( checkDataBytes2 , & checkData2 )
require . NoError ( t , err )
assert . Equal ( t , true , checkData2 [ "available" ] )
}
// TestAuthFlow_ResendVerification tests resending verification email
func TestAuthFlow_ResendVerification ( t * testing . T ) {
router , _ , _ , _ , _ , _ , cleanup := setupAuthIntegrationTestRouter ( t )
defer cleanup ( )
// Step 1: Register a user
registerReq := dto . RegisterRequest {
Email : "test4@example.com" ,
Username : "testuser4" ,
2026-01-13 18:47:57 +00:00
Password : "SecurePassword123!" ,
2025-12-25 00:35:38 +00:00
PasswordConfirm : "SecurePassword123!" ,
}
registerBody , _ := json . Marshal ( registerReq )
registerHTTPReq := httptest . NewRequest ( http . MethodPost , "/auth/register" , bytes . NewBuffer ( registerBody ) )
registerHTTPReq . Header . Set ( "Content-Type" , "application/json" )
registerW := httptest . NewRecorder ( )
router . ServeHTTP ( registerW , registerHTTPReq )
assert . Equal ( t , http . StatusCreated , registerW . Code )
// Step 2: Resend verification email
resendReq := dto . ResendVerificationRequest {
Email : "test4@example.com" ,
}
resendBody , _ := json . Marshal ( resendReq )
resendHTTPReq := httptest . NewRequest ( http . MethodPost , "/auth/resend-verification" , bytes . NewBuffer ( resendBody ) )
resendHTTPReq . Header . Set ( "Content-Type" , "application/json" )
resendW := httptest . NewRecorder ( )
router . ServeHTTP ( resendW , resendHTTPReq )
assert . Equal ( t , http . StatusOK , resendW . Code )
var resendResponse APIResponse
err := json . Unmarshal ( resendW . Body . Bytes ( ) , & resendResponse )
require . NoError ( t , err )
assert . True ( t , resendResponse . Success )
}
// TestAuthFlow_InvalidRefreshToken tests refresh with invalid token
func TestAuthFlow_InvalidRefreshToken ( t * testing . T ) {
router , _ , _ , _ , _ , _ , cleanup := setupAuthIntegrationTestRouter ( t )
defer cleanup ( )
// Try to refresh with invalid token
refreshReq := dto . RefreshRequest {
RefreshToken : "invalid-refresh-token" ,
}
refreshBody , _ := json . Marshal ( refreshReq )
refreshHTTPReq := httptest . NewRequest ( http . MethodPost , "/auth/refresh" , bytes . NewBuffer ( refreshBody ) )
refreshHTTPReq . Header . Set ( "Content-Type" , "application/json" )
refreshW := httptest . NewRecorder ( )
router . ServeHTTP ( refreshW , refreshHTTPReq )
assert . Equal ( t , http . StatusUnauthorized , refreshW . Code )
}
// TestAuthFlow_DuplicateRegistration tests registering with existing email/username
func TestAuthFlow_DuplicateRegistration ( t * testing . T ) {
router , _ , _ , _ , _ , _ , cleanup := setupAuthIntegrationTestRouter ( t )
defer cleanup ( )
// Step 1: Register a user
registerReq := dto . RegisterRequest {
Email : "test5@example.com" ,
Username : "testuser5" ,
2026-01-13 18:47:57 +00:00
Password : "SecurePassword123!" ,
2025-12-25 00:35:38 +00:00
PasswordConfirm : "SecurePassword123!" ,
}
registerBody , _ := json . Marshal ( registerReq )
registerHTTPReq := httptest . NewRequest ( http . MethodPost , "/auth/register" , bytes . NewBuffer ( registerBody ) )
registerHTTPReq . Header . Set ( "Content-Type" , "application/json" )
registerW := httptest . NewRecorder ( )
router . ServeHTTP ( registerW , registerHTTPReq )
assert . Equal ( t , http . StatusCreated , registerW . Code )
// Step 2: Try to register again with same email (should fail)
registerHTTPReq2 := httptest . NewRequest ( http . MethodPost , "/auth/register" , bytes . NewBuffer ( registerBody ) )
registerHTTPReq2 . Header . Set ( "Content-Type" , "application/json" )
registerW2 := httptest . NewRecorder ( )
router . ServeHTTP ( registerW2 , registerHTTPReq2 )
assert . Equal ( t , http . StatusConflict , registerW2 . Code )
}