veza/veza-backend-api/internal/handlers/auth_integration_test.go
2026-03-05 19:22:31 +01:00

505 lines
18 KiB
Go

//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)
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)
// Create AuthService
authService := auth.NewAuthService(
db,
emailValidator,
passwordValidator,
passwordService,
jwtService,
refreshTokenService,
emailVerificationService,
passwordResetService,
emailService,
nil, // jobWorker - not needed for integration tests
nil, // refreshLock - not needed for integration tests
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))
// 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
}
// Extract token (simplified - in real app, would validate JWT)
if !strings.HasPrefix(authHeader, "Bearer ") {
RespondWithAppError(c, apperrors.NewUnauthorizedError("Invalid authorization header"))
c.Abort()
return
}
token := strings.TrimPrefix(authHeader, "Bearer ")
// Validate token and extract user ID
claims, err := authService.JWTService.ValidateToken(token)
if err != nil {
RespondWithAppError(c, apperrors.NewUnauthorizedError("Invalid token"))
c.Abort()
return
}
// Extract user ID from claims
uid := claims.UserID
if uid == uuid.Nil {
RespondWithAppError(c, apperrors.NewUnauthorizedError("Invalid token claims"))
c.Abort()
return
}
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",
Password: "SecurePassword123!",
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 {
// INT-04: Skip - email verification token not found (integration setup/data race).
t.Skip("email verification token not found in database")
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",
Password: "SecurePassword123!",
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 {
// INT-04: Skip - email verification token not found (integration setup/data race).
t.Skip("email verification token not found in database")
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",
Password: "SecurePassword123!",
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",
Password: "SecurePassword123!",
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",
Password: "SecurePassword123!",
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)
}