2026-02-27 08:58:53 +00:00
//go:build integration
// +build integration
package integration
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"veza-backend-api/internal/config"
"veza-backend-api/internal/core/auth"
"veza-backend-api/internal/database"
"veza-backend-api/internal/dto"
"veza-backend-api/internal/handlers"
"veza-backend-api/internal/middleware"
"veza-backend-api/internal/models"
"veza-backend-api/internal/repositories"
"veza-backend-api/internal/services"
"veza-backend-api/internal/validators"
)
// setupLogoutBlacklistTestRouter creates a test router with AuthMiddleware + TokenBlacklist
func setupLogoutBlacklistTestRouter ( t * testing . T ) ( * gin . Engine , * auth . AuthService , * services . TokenBlacklist , * gorm . DB , func ( ) ) {
gin . SetMode ( gin . TestMode )
logger := zaptest . NewLogger ( t )
// Redis for TokenBlacklist
redisAddr := os . Getenv ( "REDIS_ADDR" )
if redisAddr == "" {
redisAddr = "localhost:6379"
}
redisClient := redis . NewClient ( & redis . Options { Addr : redisAddr } )
ctx := context . Background ( )
if err := redisClient . Ping ( ctx ) . Err ( ) ; err != nil {
t . Skipf ( "Skipping test: Redis not available at %s: %v" , redisAddr , err )
return nil , nil , nil , nil , func ( ) { }
}
tokenBlacklist := services . NewTokenBlacklist ( redisClient )
db , err := gorm . Open ( sqlite . Open ( ":memory:" ) , & gorm . Config { } )
require . NoError ( t , err )
db . Exec ( "PRAGMA foreign_keys = ON" )
err = db . AutoMigrate (
& models . User { } ,
& models . RefreshToken { } ,
& models . Session { } ,
& models . Role { } ,
& models . Permission { } ,
& models . UserRole { } ,
& models . RolePermission { } ,
)
require . NoError ( t , err )
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 )
sqlDB , err := db . DB ( )
require . NoError ( t , err )
dbWrapper := & database . Database { DB : sqlDB , GormDB : db , Logger : logger }
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" )
2026-02-27 08:58:53 +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 )
authService := auth . NewAuthService (
db , emailValidator , passwordValidator , passwordService ,
jwtService , refreshTokenService , emailVerificationService ,
passwordResetService , emailService , nil , nil , logger ,
)
sessionService := services . NewSessionService ( dbWrapper , logger )
twoFactorService := services . NewTwoFactorService ( dbWrapper , logger )
userRepo := repositories . NewGormUserRepository ( db )
userService := services . NewUserServiceWithDB ( userRepo , db )
auditService := services . NewAuditService ( dbWrapper , logger )
permissionService := services . NewPermissionService ( db )
authMiddleware := middleware . NewAuthMiddleware (
sessionService ,
auditService ,
permissionService ,
jwtService ,
userService ,
nil , // apiKeyService
tokenBlacklist ,
logger ,
)
cfg := & config . Config {
CookiePath : "/" ,
CookieDomain : "" ,
CookieHttpOnly : true ,
CookieSecure : false ,
CookieSameSite : "lax" ,
JWTService : jwtService ,
TokenBlacklist : tokenBlacklist ,
}
router := gin . New ( )
authGroup := router . Group ( "/auth" )
{
authGroup . POST ( "/login" , handlers . Login ( authService , sessionService , twoFactorService , logger , cfg ) )
authGroup . POST ( "/register" , handlers . Register ( authService , sessionService , logger , cfg ) )
authGroup . POST ( "/refresh" , handlers . Refresh ( authService , sessionService , logger , cfg ) )
authGroup . POST ( "/verify-email" , handlers . VerifyEmail ( authService ) )
authGroup . GET ( "/check-username" , handlers . CheckUsername ( authService ) )
protected := authGroup . Group ( "" )
protected . Use ( authMiddleware . RequireAuth ( ) )
protected . POST ( "/logout" , handlers . Logout ( authService , sessionService , logger , cfg ) )
protected . GET ( "/me" , handlers . GetMe ( userService ) )
}
cleanup := func ( ) {
redisClient . FlushDB ( ctx )
redisClient . Close ( )
}
return router , authService , tokenBlacklist , db , cleanup
}
// TestLogoutBlacklist tests that after logout, the access token is blacklisted and returns 401
func TestLogoutBlacklist ( t * testing . T ) {
router , _ , _ , db , cleanup := setupLogoutBlacklistTestRouter ( t )
defer cleanup ( )
if router == nil {
return
}
// 1. Register
registerBody , _ := json . Marshal ( dto . RegisterRequest {
Email : "blacklist@test.com" ,
Username : "blacklisttest" ,
Password : "SecurePassword123!" ,
PasswordConfirm : "SecurePassword123!" ,
} )
registerReq := httptest . NewRequest ( http . MethodPost , "/auth/register" , bytes . NewBuffer ( registerBody ) )
registerReq . Header . Set ( "Content-Type" , "application/json" )
registerW := httptest . NewRecorder ( )
router . ServeHTTP ( registerW , registerReq )
require . Equal ( t , http . StatusCreated , registerW . Code )
// 2. Verify email
var user models . User
require . NoError ( t , db . Where ( "email = ?" , "blacklist@test.com" ) . First ( & user ) . Error )
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 {
t . Skip ( "email verification token not found" )
return
}
verifyReq := httptest . NewRequest ( http . MethodPost , "/auth/verify-email?token=" + token , nil )
verifyW := httptest . NewRecorder ( )
router . ServeHTTP ( verifyW , verifyReq )
require . Equal ( t , http . StatusOK , verifyW . Code )
// 3. Login
loginBody , _ := json . Marshal ( dto . LoginRequest {
Email : "blacklist@test.com" ,
Password : "SecurePassword123!" ,
RememberMe : false ,
} )
loginReq := httptest . NewRequest ( http . MethodPost , "/auth/login" , bytes . NewBuffer ( loginBody ) )
loginReq . Header . Set ( "Content-Type" , "application/json" )
loginW := httptest . NewRecorder ( )
router . ServeHTTP ( loginW , loginReq )
require . Equal ( t , http . StatusOK , loginW . Code )
// Extract tokens from response and cookies
var loginResp handlers . APIResponse
require . NoError ( t , json . Unmarshal ( loginW . Body . Bytes ( ) , & loginResp ) )
loginDataBytes , _ := json . Marshal ( loginResp . Data )
var loginData dto . LoginResponse
require . NoError ( t , json . Unmarshal ( loginDataBytes , & loginData ) )
accessToken := loginData . Token . AccessToken
require . NotEmpty ( t , accessToken )
var refreshCookie * http . Cookie
for _ , c := range loginW . Result ( ) . Cookies ( ) {
if c . Name == "refresh_token" {
refreshCookie = c
break
}
}
require . NotNil ( t , refreshCookie )
// 4. Access /me with access token -> 200
meReq := httptest . NewRequest ( http . MethodGet , "/auth/me" , nil )
meReq . Header . Set ( "Authorization" , "Bearer " + accessToken )
meW := httptest . NewRecorder ( )
router . ServeHTTP ( meW , meReq )
assert . Equal ( t , http . StatusOK , meW . Code )
// 5. Logout (with access token in Authorization and refresh in cookie)
logoutReq := httptest . NewRequest ( http . MethodPost , "/auth/logout" , nil )
logoutReq . Header . Set ( "Authorization" , "Bearer " + accessToken )
logoutReq . AddCookie ( refreshCookie )
logoutW := httptest . NewRecorder ( )
router . ServeHTTP ( logoutW , logoutReq )
assert . Equal ( t , http . StatusOK , logoutW . Code )
// 6. Access /me with SAME access token -> 401 (blacklisted)
meReq2 := httptest . NewRequest ( http . MethodGet , "/auth/me" , nil )
meReq2 . Header . Set ( "Authorization" , "Bearer " + accessToken )
meW2 := httptest . NewRecorder ( )
router . ServeHTTP ( meW2 , meReq2 )
assert . Equal ( t , http . StatusUnauthorized , meW2 . Code , "Blacklisted token should return 401" )
// 7. Refresh with old refresh token -> 401 (invalidated)
refreshReq := httptest . NewRequest ( http . MethodPost , "/auth/refresh" , nil )
refreshReq . AddCookie ( refreshCookie )
refreshW := httptest . NewRecorder ( )
router . ServeHTTP ( refreshW , refreshReq )
assert . Equal ( t , http . StatusUnauthorized , refreshW . Code , "Revoked refresh token should return 401" )
}