505 lines
18 KiB
Go
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(), ®isterResponse)
|
|
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)
|
|
}
|