//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) }