250 lines
8.3 KiB
Go
250 lines
8.3 KiB
Go
//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)
|
|
jwtService, err := services.NewJWTService("", "", "test-secret-key-must-be-32-chars-long", "test-issuer", "test-audience")
|
|
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")
|
|
}
|