package auth import ( "context" "testing" "time" "veza-backend-api/internal/models" "veza-backend-api/internal/validators" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" "gorm.io/driver/sqlite" "gorm.io/gorm" ) type TestMocks struct { JWT *MockJWTService EmailVerification *MockEmailVerificationService RefreshToken *MockRefreshTokenService PasswordReset *MockPasswordResetService Password *MockPasswordService Email *MockEmailService JobWorker *MockJobWorker } func setupTestAuthService(t *testing.T) (*AuthService, *gorm.DB, *TestMocks, func()) { logger := zaptest.NewLogger(t) // Setup in-memory SQLite database db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ // Logger: logger.Default.LogMode(logger.Silent), }) require.NoError(t, err) // Enable foreign keys db.Exec("PRAGMA foreign_keys = ON") // Auto-migrate models err = db.AutoMigrate( &models.User{}, &models.RefreshToken{}, &models.Role{}, ) require.NoError(t, err) // Setup Database wrapper sqlDB, err := db.DB() require.NoError(t, err) // dbWrapper removed as it was unused (EmailValidator uses db directly) emailValidator := validators.NewEmailValidator(db) // validators.NewEmailValidator expects *database.Database (which wraps sql.DB) or *gorm.DB? // Checking the file previously: validators.NewEmailValidator(db) where db was *gorm.DB in previous code... // Wait, previous code: // 58: emailValidator := validators.NewEmailValidator(db) // And db was *gorm.DB. So NewEmailValidator likely takes *gorm.DB. // But in PasswordService it took dbWrapper (*database.Database). // Let's assume *gorm.DB for emailValidator based on previous code. passwordValidator := validators.NewPasswordValidator() mocks := &TestMocks{ JWT: &MockJWTService{}, EmailVerification: &MockEmailVerificationService{}, RefreshToken: &MockRefreshTokenService{}, PasswordReset: &MockPasswordResetService{}, Password: &MockPasswordService{}, Email: &MockEmailService{}, JobWorker: &MockJobWorker{}, } mocks.JWT.On("GetConfig").Return(nil).Maybe() // Default config service := NewAuthService( db, emailValidator, passwordValidator, mocks.Password, mocks.JWT, mocks.RefreshToken, mocks.EmailVerification, mocks.PasswordReset, mocks.Email, mocks.JobWorker, logger, ) cleanup := func() { sqlDB.Close() } return service, db, mocks, cleanup } func TestAuthService_VerifyEmail(t *testing.T) { service, db, mocks, cleanup := setupTestAuthService(t) defer cleanup() ctx := context.Background() // Create user user := models.User{ ID: uuid.New(), Email: "verify@example.com", Username: "verifyuser", Role: "user", IsActive: true, IsVerified: false, CreatedAt: time.Now(), UpdatedAt: time.Now(), } err := db.Create(&user).Error require.NoError(t, err) token := "valid-token" mocks.EmailVerification.On("VerifyToken", token).Return(user.ID, nil) mocks.EmailVerification.On("InvalidateOldTokens", user.ID).Return(nil) err = service.VerifyEmail(ctx, token) require.NoError(t, err) var updatedUser models.User err = db.First(&updatedUser, user.ID).Error require.NoError(t, err) assert.True(t, updatedUser.IsVerified) mocks.EmailVerification.AssertExpectations(t) } func TestAuthService_ResendVerificationEmail(t *testing.T) { service, db, mocks, cleanup := setupTestAuthService(t) defer cleanup() ctx := context.Background() user := models.User{ ID: uuid.New(), Email: "resend@example.com", Username: "resenduser", Role: "user", IsVerified: false, CreatedAt: time.Now(), UpdatedAt: time.Now(), } db.Create(&user) token := "new-token" mocks.EmailVerification.On("InvalidateOldTokens", user.ID).Return(nil) mocks.EmailVerification.On("GenerateToken").Return(token, nil) mocks.EmailVerification.On("StoreToken", user.ID, user.Email, token).Return(nil) // Implementation logs "Send verification email" but doesn't seem to call EmailService if it uses EmailVerificationService? // Checking code: // if s.emailVerificationService != nil { ... StoreToken ... logger.Info("Sending verification email") } // It basically assumes StoreToken or internal logic sends it? // Wait, the `ResendVerificationEmail` implementation in `service.go` logic: /* if err := s.emailVerificationService.StoreToken(user.ID, user.Email, token); err != nil { return err } s.logger.Info("Resending verification email", ...) return nil */ // It doesn't verify strict sending via EmailService in the provided code snippet, it just logs. // Ah, wait, in Register() it has logic to send email. In Resend it seems to miss the actual sending call? // Let's check `service.go` lines 589+. // It calls `s.emailVerificationService.StoreToken`. // Does `StoreToken` send the email? No, `EmailVerificationService` just stores. // So `ResendVerificationEmail` might be missing the `SendVerificationEmail` call? // Or maybe I missed it in my view. // Let's assume for now it logic is as viewed: Invalidate -> Generate -> Store. err := service.ResendVerificationEmail(ctx, user.Email) require.NoError(t, err) mocks.EmailVerification.AssertExpectations(t) } func TestAuthService_RequestPasswordReset(t *testing.T) { service, db, mocks, cleanup := setupTestAuthService(t) defer cleanup() ctx := context.Background() user := models.User{ ID: uuid.New(), Email: "reset@example.com", Username: "resetuser", Role: "user", CreatedAt: time.Now(), UpdatedAt: time.Now(), } db.Create(&user) token := "reset-token" mocks.PasswordReset.On("InvalidateOldTokens", user.ID).Return(nil) mocks.PasswordReset.On("GenerateToken").Return(token, nil) mocks.PasswordReset.On("StoreToken", user.ID, token).Return(nil) // It uses jobWorker to send email if available mocks.JobWorker.On("EnqueueEmailJobWithTemplate", user.Email, "Reset your Veza password", "password_reset", mock.AnythingOfType("map[string]interface {}")).Return() err := service.RequestPasswordReset(ctx, user.Email) require.NoError(t, err) mocks.PasswordReset.AssertExpectations(t) mocks.JobWorker.AssertExpectations(t) } func TestAuthService_ResetPassword(t *testing.T) { service, _, mocks, cleanup := setupTestAuthService(t) defer cleanup() ctx := context.Background() token := "valid-reset-token" newPassword := "NewStrongPass1!" userID := uuid.New() mocks.PasswordReset.On("VerifyToken", token).Return(userID, nil) mocks.Password.On("ValidatePassword", newPassword).Return(nil) mocks.Password.On("UpdatePassword", userID, newPassword).Return(nil) mocks.PasswordReset.On("MarkTokenAsUsed", token).Return(nil) mocks.RefreshToken.On("RevokeAll", userID).Return(nil) err := service.ResetPassword(ctx, token, newPassword) require.NoError(t, err) mocks.PasswordReset.AssertExpectations(t) mocks.Password.AssertExpectations(t) } func TestAuthService_AdminVerifyUser(t *testing.T) { service, db, mocks, cleanup := setupTestAuthService(t) defer cleanup() ctx := context.Background() user := models.User{ ID: uuid.New(), Email: "admin_verify@example.com", Username: "adminverify", IsVerified: false, CreatedAt: time.Now(), UpdatedAt: time.Now(), } db.Create(&user) mocks.EmailVerification.On("InvalidateOldTokens", user.ID).Return(nil) err := service.AdminVerifyUser(ctx, user.ID) require.NoError(t, err) var updatedUser models.User db.First(&updatedUser, user.ID) assert.True(t, updatedUser.IsVerified) mocks.EmailVerification.AssertExpectations(t) } func TestAuthService_AdminBlockUser(t *testing.T) { service, _, mocks, cleanup := setupTestAuthService(t) defer cleanup() ctx := context.Background() userID := uuid.New() mocks.RefreshToken.On("RevokeAll", userID).Return(nil) err := service.AdminBlockUser(ctx, userID) require.NoError(t, err) mocks.RefreshToken.AssertExpectations(t) } func TestAuthService_InvalidateAllUserSessions(t *testing.T) { service, _, mocks, cleanup := setupTestAuthService(t) defer cleanup() ctx := context.Background() userID := uuid.New() mocks.RefreshToken.On("RevokeAll", userID).Return(nil) // Calls InvalidateAllUserSessions with nil sessionService for now or mock it? // The function signature takes interface{ RevokeAllUserSessions... } // We can pass nil. err := service.InvalidateAllUserSessions(ctx, userID, nil) require.NoError(t, err) mocks.RefreshToken.AssertExpectations(t) } func TestAuthService_Logout(t *testing.T) { service, _, mocks, cleanup := setupTestAuthService(t) defer cleanup() ctx := context.Background() userID := uuid.New() refreshToken := "valid-refresh-token" claims := &models.CustomClaims{ UserID: userID, } mocks.JWT.On("ValidateToken", refreshToken).Return(claims, nil) mocks.RefreshToken.On("Revoke", userID, refreshToken).Return(nil) err := service.Logout(ctx, userID, refreshToken) require.NoError(t, err) mocks.JWT.AssertExpectations(t) mocks.RefreshToken.AssertExpectations(t) } func TestAuthService_Login_Success(t *testing.T) { service, _, mocks, cleanup := setupTestAuthService(t) defer cleanup() ctx := context.Background() email := "login_mock@example.com" password := "StrongPass1!" // Manually insert user with hashed password since we mock PasswordService in constructor but Register uses bcrypt direct? // Wait, Register uses bcrypt.GenerateFromPassword directly in `service.go`. // Login uses `bcrypt.CompareHashAndPassword` directly too. // So mocking `PasswordService` doesn't affect `Login` or `Register` unless refactored to use it. // But `AuthService` constructor accepts `passwordService` and uses it for `ResetPassword`. // `Register` and `Login` use `bcrypt` directly. This is potential refactoring debt but for now we follow existing logic. // Create user with bcrypt-hashed password // hashed, _ := services.NewPasswordService(nil, zap.NewNop()).Hash(password) // Using real helper or direct bcrypt // Easier: use bcrypt directly ? // Or just use the one from `setupTestAuthService` but we mocked it. // Let's use direct code: // ... imports needed for bcrypt ... // Since I can't easily import bcrypt here without modifying imports, I'll rely on the fact that `Register` (which uses bcrypt) covers hashing. // But `Register` uses `mocks.JWT` which I need to set up. mocks.JWT.On("GenerateAccessToken", mock.AnythingOfType("*models.User")).Return("access-token", nil).Once() mocks.JWT.On("GenerateRefreshToken", mock.AnythingOfType("*models.User")).Return("refresh-token", nil).Once() mocks.RefreshToken.On("Store", mock.AnythingOfType("uuid.UUID"), "refresh-token", mock.Anything).Return(nil).Once() mocks.EmailVerification.On("GenerateToken").Return("verify-token", nil).Once() mocks.EmailVerification.On("StoreToken", mock.AnythingOfType("uuid.UUID"), email, "verify-token").Return(nil).Once() user, _, err := service.Register(ctx, email, "loginuser", password) require.NoError(t, err) // Now Login // Login also needs JWT generation expectations mocks.JWT.On("GenerateAccessToken", mock.AnythingOfType("*models.User")).Return("new-access-token", nil).Once() mocks.JWT.On("GenerateRefreshToken", mock.AnythingOfType("*models.User")).Return("new-refresh-token", nil).Once() mocks.RefreshToken.On("Store", user.ID, "new-refresh-token", mock.Anything).Return(nil).Once() loggedInUser, tokens, err := service.Login(ctx, email, password, false) require.NoError(t, err) assert.Equal(t, user.ID, loggedInUser.ID) assert.Equal(t, "new-access-token", tokens.AccessToken) mocks.JWT.AssertExpectations(t) }