package handlers import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "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/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" ) // setupAuthTestRouter creates a test router with real auth service and mocked dependencies. // Optional authMiddlewares are applied to the /auth group before routes (for tests needing user_id in context). func setupAuthTestRouter(t *testing.T, authMiddlewares ...gin.HandlerFunc) (*gin.Engine, *auth.AuthService, *services.SessionService, *services.TwoFactorService, *services.UserService, *zap.Logger, func(), *gorm.DB) { 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 models err = db.AutoMigrate( &models.User{}, &models.RefreshToken{}, &models.Session{}, ) 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 // Get underlying SQL DB for services that need it (like EmailVerificationService) sqlDB, err := db.DB() require.NoError(t, err) dbWrapper.DB = sqlDB // Add 2FA columns to users (SQLite test DB; GORM User model may not have them) // Ignore errors if columns already exist (e.g. from a future GORM migration) _, _ = sqlDB.Exec("ALTER TABLE users ADD COLUMN two_factor_enabled BOOLEAN DEFAULT 0") _, _ = sqlDB.Exec("ALTER TABLE users ADD COLUMN two_factor_secret TEXT DEFAULT ''") _, _ = sqlDB.Exec("ALTER TABLE users ADD COLUMN backup_codes TEXT DEFAULT '[]'") // 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 unit tests nil, // refreshLock - not needed for unit 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) // Create default config cfg := &config.Config{} router := gin.New() authGroup := router.Group("/auth") for _, mw := range authMiddlewares { authGroup.Use(mw) } { authGroup.POST("/login", Login(authService, sessionService, twoFactorService, logger, cfg)) authGroup.POST("/register", Register(authService, sessionService, logger, cfg)) authGroup.POST("/refresh", Refresh(authService, sessionService, logger, cfg)) authGroup.POST("/logout", Logout(authService, sessionService, logger, cfg)) authGroup.POST("/verify-email", VerifyEmail(authService)) authGroup.POST("/resend-verification", ResendVerification(authService, logger)) authGroup.GET("/check-username", CheckUsername(authService)) authGroup.GET("/me", GetMe(userService)) } cleanup := func() { // Database cleanup handled by test } return router, authService, sessionService, twoFactorService, userService, logger, cleanup, db } func TestLogin_Success(t *testing.T) { router, authService, _, _, _, _, cleanup, db := setupAuthTestRouter(t) defer cleanup() // Create a test user first ctx := context.Background() user, err := authService.Register(ctx, "test@example.com", "testuser", "SecurePassword123!") require.NoError(t, err) require.NotNil(t, user) // Verify email to allow login db.Model(&models.User{}).Where("id = ?", user.ID).Update("is_verified", true) reqBody := dto.LoginRequest{ Email: "test@example.com", Password: "SecurePassword123!", RememberMe: false, } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/auth/login", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var response APIResponse json.Unmarshal(w.Body.Bytes(), &response) assert.True(t, response.Success) } func TestLogin_InvalidCredentials(t *testing.T) { router, authService, _, _, _, _, cleanup, db := setupAuthTestRouter(t) defer cleanup() // Create a test user first ctx := context.Background() user, err := authService.Register(ctx, "test@example.com", "testuser", "SecurePassword123!") require.NoError(t, err) require.NotNil(t, user) // Verify email db.Model(&models.User{}).Where("id = ?", user.ID).Update("is_verified", true) reqBody := dto.LoginRequest{ Email: "test@example.com", Password: "wrongpassword", RememberMe: false, } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/auth/login", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) // invalid credentials -> 401 (auth.go returns 401) } func TestLogin_EmailNotVerified(t *testing.T) { router, authService, _, _, _, _, cleanup, _ := setupAuthTestRouter(t) defer cleanup() // Create a test user but don't verify email ctx := context.Background() user, err := authService.Register(ctx, "test@example.com", "testuser", "SecurePassword123!") require.NoError(t, err) require.NotNil(t, user) // User is not verified by default (v1.0.4: Register leaves is_verified=false). reqBody := dto.LoginRequest{ Email: "test@example.com", Password: "SecurePassword123!", RememberMe: false, } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/auth/login", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Login refuses unverified users; the user must POST /auth/verify-email first. assert.Equal(t, http.StatusForbidden, w.Code) } func TestLogin_Requires2FA(t *testing.T) { router, authService, _, twoFactorService, _, _, cleanup, db := setupAuthTestRouter(t) defer cleanup() // Create a test user ctx := context.Background() user, err := authService.Register(ctx, "test@example.com", "testuser", "SecurePassword123!") require.NoError(t, err) require.NotNil(t, user) // Verify email - use GORM directly db.Model(&models.User{}).Where("id = ?", user.ID).Update("is_verified", true) // Enable 2FA for user - generate secret and enable setup, err := twoFactorService.GenerateSecret(user) require.NoError(t, err) err = twoFactorService.EnableTwoFactor(ctx, user.ID, setup.Secret, setup.RecoveryCodes) require.NoError(t, err) reqBody := dto.LoginRequest{ Email: "test@example.com", Password: "SecurePassword123!", RememberMe: false, } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/auth/login", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var response APIResponse json.Unmarshal(w.Body.Bytes(), &response) assert.True(t, response.Success) // Check that response contains requires_2fa flag var loginResponse dto.LoginResponse responseData, _ := json.Marshal(response.Data) json.Unmarshal(responseData, &loginResponse) assert.True(t, loginResponse.Requires2FA) } func TestLogin_InvalidRequest(t *testing.T) { router, _, _, _, _, _, cleanup, _ := setupAuthTestRouter(t) defer cleanup() // Missing email reqBody := map[string]interface{}{ "password": "password123", } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/auth/login", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestRegister_Success(t *testing.T) { router, _, _, _, _, _, cleanup, _ := setupAuthTestRouter(t) defer cleanup() reqBody := dto.RegisterRequest{ Email: "newuser@example.com", Username: "newuser", Password: "SecurePassword123!", PasswordConfirm: "SecurePassword123!", } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/auth/register", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusCreated, w.Code) var response APIResponse json.Unmarshal(w.Body.Bytes(), &response) assert.True(t, response.Success) } func TestRegister_UserAlreadyExists(t *testing.T) { router, authService, _, _, _, _, cleanup, _ := setupAuthTestRouter(t) defer cleanup() // Create a user first ctx := context.Background() _, err := authService.Register(ctx, "existing@example.com", "existinguser", "SecurePassword123!") require.NoError(t, err) // Try to register again with same email reqBody := dto.RegisterRequest{ Email: "existing@example.com", Username: "existinguser2", Password: "SecurePassword123!", PasswordConfirm: "SecurePassword123!", } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/auth/register", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusConflict, w.Code) } func TestRegister_InvalidEmail(t *testing.T) { router, _, _, _, _, _, cleanup, _ := setupAuthTestRouter(t) defer cleanup() // Invalid email format will be caught by validation reqBody := dto.RegisterRequest{ Email: "invalid-email", Username: "newuser", Password: "SecurePassword123!", PasswordConfirm: "SecurePassword123!", } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/auth/register", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Should fail validation before reaching service assert.Equal(t, http.StatusBadRequest, w.Code) } func TestRegister_WeakPassword(t *testing.T) { router, _, _, _, _, _, cleanup, _ := setupAuthTestRouter(t) defer cleanup() // Weak password will be caught by validation (min=12) reqBody := dto.RegisterRequest{ Email: "newuser@example.com", Username: "newuser", Password: "weak", PasswordConfirm: "weak", } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/auth/register", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Should fail validation before reaching service assert.Equal(t, http.StatusBadRequest, w.Code) } func TestRegister_InvalidRequest(t *testing.T) { router, _, _, _, _, _, cleanup, _ := setupAuthTestRouter(t) defer cleanup() // Missing required fields reqBody := map[string]interface{}{ "email": "test@example.com", } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/auth/register", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestRefresh_InvalidRequest(t *testing.T) { router, _, _, _, _, _, cleanup, _ := setupAuthTestRouter(t) defer cleanup() // Missing refresh_token reqBody := map[string]interface{}{} body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/auth/refresh", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestLogout_Success(t *testing.T) { userID := uuid.New() router, _, _, _, _, _, cleanup, _ := setupAuthTestRouter(t, func(c *gin.Context) { if c.Request.URL.Path == "/auth/logout" { c.Set("user_id", userID) } c.Next() }) defer cleanup() reqBody := map[string]interface{}{ "refresh_token": "refresh_token", } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/auth/logout", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer access_token") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestLogout_Unauthorized(t *testing.T) { router, _, _, _, _, _, cleanup, _ := setupAuthTestRouter(t) defer cleanup() reqBody := map[string]interface{}{ "refresh_token": "refresh_token", } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/auth/logout", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } func TestVerifyEmail_MissingToken(t *testing.T) { router, _, _, _, _, _, cleanup, _ := setupAuthTestRouter(t) defer cleanup() req := httptest.NewRequest(http.MethodPost, "/auth/verify-email", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestResendVerification_Success(t *testing.T) { router, authService, _, _, _, _, cleanup, db := setupAuthTestRouter(t) defer cleanup() // Create a test user (not verified) - Register creates with is_verified=true by default, so we set false ctx := context.Background() user, err := authService.Register(ctx, "test@example.com", "testuser", "SecurePassword123!") require.NoError(t, err) db.Model(&models.User{}).Where("id = ?", user.ID).Update("is_verified", false) reqBody := dto.ResendVerificationRequest{ Email: "test@example.com", } body, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/auth/resend-verification", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestCheckUsername_Available(t *testing.T) { router, _, _, _, _, _, cleanup, _ := setupAuthTestRouter(t) defer cleanup() req := httptest.NewRequest(http.MethodGet, "/auth/check-username?username=newusername", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var response APIResponse json.Unmarshal(w.Body.Bytes(), &response) assert.True(t, response.Success) var data map[string]interface{} responseData, _ := json.Marshal(response.Data) json.Unmarshal(responseData, &data) assert.True(t, data["available"].(bool)) } func TestCheckUsername_Taken(t *testing.T) { router, authService, _, _, _, _, cleanup, _ := setupAuthTestRouter(t) defer cleanup() // Create a user with username ctx := context.Background() _, err := authService.Register(ctx, "test@example.com", "existinguser", "SecurePassword123!") require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "/auth/check-username?username=existinguser", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var response APIResponse json.Unmarshal(w.Body.Bytes(), &response) assert.True(t, response.Success) var data map[string]interface{} responseData, _ := json.Marshal(response.Data) json.Unmarshal(responseData, &data) assert.False(t, data["available"].(bool)) } func TestCheckUsername_MissingUsername(t *testing.T) { router, _, _, _, _, _, cleanup, _ := setupAuthTestRouter(t) defer cleanup() req := httptest.NewRequest(http.MethodGet, "/auth/check-username", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestGetMe_Success(t *testing.T) { var userID uuid.UUID router, authService, _, _, _, _, cleanup, _ := setupAuthTestRouter(t, func(c *gin.Context) { if c.Request.URL.Path == "/auth/me" { c.Set("user_id", userID) } c.Next() }) defer cleanup() // Create a test user ctx := context.Background() user, err := authService.Register(ctx, "test@example.com", "testuser", "SecurePassword123!") require.NoError(t, err) userID = user.ID req := httptest.NewRequest(http.MethodGet, "/auth/me", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestGetMe_Unauthorized(t *testing.T) { router, _, _, _, _, _, cleanup, _ := setupAuthTestRouter(t) defer cleanup() req := httptest.NewRequest(http.MethodGet, "/auth/me", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) }