package services import ( "context" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "golang.org/x/crypto/bcrypt" "gorm.io/driver/sqlite" "gorm.io/gorm" "veza-backend-api/internal/database" ) func setupTestPasswordServiceIntegration(t *testing.T) (*PasswordService, *gorm.DB, *database.Database) { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) db.Exec("PRAGMA foreign_keys = ON") // Create users table err = db.Exec(` CREATE TABLE users ( id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, username TEXT NOT NULL, password_hash TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `).Error require.NoError(t, err) // Create password_reset_tokens table err = db.Exec(` CREATE TABLE password_reset_tokens ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, token TEXT NOT NULL UNIQUE, expires_at TIMESTAMP NOT NULL, used INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `).Error require.NoError(t, err) sqlDB, err := db.DB() require.NoError(t, err) testDB := &database.Database{ DB: sqlDB, } logger := zap.NewNop() service := NewPasswordService(testDB, logger) return service, db, testDB } func createTestUser(t *testing.T, testDB *database.Database, userID uuid.UUID, email, username, passwordHash string) { ctx := context.Background() _, err := testDB.ExecContext(ctx, ` INSERT INTO users (id, email, username, password_hash) VALUES ($1, $2, $3, $4) `, userID.String(), email, username, passwordHash) require.NoError(t, err) } func TestPasswordService_GetUserByEmail_Success(t *testing.T) { service, _, testDB := setupTestPasswordServiceIntegration(t) userID := uuid.New() email := "test@example.com" username := "testuser" passwordHash := "hashedpassword" createTestUser(t, testDB, userID, email, username, passwordHash) user, err := service.GetUserByEmail(email) assert.NoError(t, err) assert.NotNil(t, user) assert.Equal(t, userID, user.ID) assert.Equal(t, email, user.Email) assert.Equal(t, username, user.Username) } func TestPasswordService_GetUserByEmail_NotFound(t *testing.T) { service, _, _ := setupTestPasswordServiceIntegration(t) user, err := service.GetUserByEmail("nonexistent@example.com") assert.Error(t, err) assert.Nil(t, user) assert.Contains(t, err.Error(), "user not found") } func TestPasswordService_GeneratePasswordResetToken_Success(t *testing.T) { service, _, _ := setupTestPasswordServiceIntegration(t) userID := uuid.New() token, expiresAt, err := service.GeneratePasswordResetToken(userID) assert.NoError(t, err) assert.NotEmpty(t, token) assert.True(t, expiresAt.After(time.Now())) assert.True(t, expiresAt.Before(time.Now().Add(2*time.Hour))) // Should be ~1 hour _ = expiresAt // Use expiresAt to avoid unused variable warning } func TestPasswordService_GeneratePasswordResetToken_StoredInDB(t *testing.T) { service, _, testDB := setupTestPasswordServiceIntegration(t) userID := uuid.New() _, _, err := service.GeneratePasswordResetToken(userID) assert.NoError(t, err) // Verify token hash is stored in database (INF-10: tokens stored hashed, not plain) ctx := context.Background() var storedTokenHash string var storedExpiresAt time.Time var storedUsed bool err = testDB.QueryRowContext(ctx, ` SELECT token, expires_at, used FROM password_reset_tokens WHERE user_id = $1 `, userID.String()).Scan(&storedTokenHash, &storedExpiresAt, &storedUsed) assert.NoError(t, err) assert.NotEmpty(t, storedTokenHash, "token hash should be stored") assert.Len(t, storedTokenHash, 64, "SHA256 hex = 64 chars") assert.False(t, storedUsed) } func TestPasswordService_ResetPassword_Success(t *testing.T) { // INT-04: Skip - requires PostgreSQL; service uses NOW() in queries, SQLite incompatible. t.Skip("requires PostgreSQL NOW() function") service, _, testDB := setupTestPasswordServiceIntegration(t) userID := uuid.New() passwordHash, _ := bcrypt.GenerateFromPassword([]byte("oldpassword"), 12) createTestUser(t, testDB, userID, "test@example.com", "testuser", string(passwordHash)) // Generate reset token token, _, err := service.GeneratePasswordResetToken(userID) require.NoError(t, err) // Reset password newPassword := "NewSecurePassword123!" err = service.ResetPassword(token, newPassword) assert.NoError(t, err) // Verify password was updated ctx := context.Background() var updatedHash string err = testDB.QueryRowContext(ctx, ` SELECT password_hash FROM users WHERE id = $1 `, userID.String()).Scan(&updatedHash) assert.NoError(t, err) // Verify new password works err = bcrypt.CompareHashAndPassword([]byte(updatedHash), []byte(newPassword)) assert.NoError(t, err) // Verify token is marked as used var used bool err = testDB.QueryRowContext(ctx, ` SELECT used FROM password_reset_tokens WHERE token = $1 `, token).Scan(&used) assert.NoError(t, err) assert.True(t, used) } func TestPasswordService_ResetPassword_InvalidToken(t *testing.T) { service, _, _ := setupTestPasswordServiceIntegration(t) err := service.ResetPassword("invalid_token", "NewPassword123!") assert.Error(t, err) assert.Contains(t, err.Error(), "invalid or expired reset token") } func TestPasswordService_ResetPassword_ExpiredToken(t *testing.T) { service, _, testDB := setupTestPasswordServiceIntegration(t) userID := uuid.New() passwordHash, _ := bcrypt.GenerateFromPassword([]byte("oldpassword"), 12) createTestUser(t, testDB, userID, "test@example.com", "testuser", string(passwordHash)) // Create expired token manually (DB stores hash, not plain token) plainToken := "expired_token" tokenHash := hashTokenForTest(plainToken) ctx := context.Background() expiredTime := time.Now().Add(-2 * time.Hour) _, err := testDB.ExecContext(ctx, ` INSERT INTO password_reset_tokens (user_id, token, expires_at, used) VALUES ($1, $2, $3, $4) `, userID.String(), tokenHash, expiredTime, false) require.NoError(t, err) err = service.ResetPassword(plainToken, "NewPassword123!") assert.Error(t, err) assert.Contains(t, err.Error(), "reset token has expired") } func TestPasswordService_ResetPassword_WeakPassword(t *testing.T) { service, _, testDB := setupTestPasswordServiceIntegration(t) userID := uuid.New() passwordHash, _ := bcrypt.GenerateFromPassword([]byte("oldpassword"), 12) createTestUser(t, testDB, userID, "test@example.com", "testuser", string(passwordHash)) // Generate reset token token, _, err := service.GeneratePasswordResetToken(userID) require.NoError(t, err) // Try to reset with weak password err = service.ResetPassword(token, "weak") assert.Error(t, err) assert.Contains(t, err.Error(), "weak password") } func TestPasswordService_ResetPassword_UsedToken(t *testing.T) { // INT-04: Skip - requires PostgreSQL; service uses NOW() in queries, SQLite incompatible. t.Skip("requires PostgreSQL NOW() function") service, _, testDB := setupTestPasswordServiceIntegration(t) userID := uuid.New() passwordHash, _ := bcrypt.GenerateFromPassword([]byte("oldpassword"), 12) createTestUser(t, testDB, userID, "test@example.com", "testuser", string(passwordHash)) // Generate reset token token, _, err := service.GeneratePasswordResetToken(userID) require.NoError(t, err) // Use token once err = service.ResetPassword(token, "NewSecurePassword123!") require.NoError(t, err) // Try to use token again err = service.ResetPassword(token, "AnotherPassword123!") assert.Error(t, err) assert.Contains(t, err.Error(), "invalid or expired reset token") } func TestPasswordService_ValidatePassword_Valid(t *testing.T) { service, _, _ := setupTestPasswordServiceIntegration(t) validPasswords := []string{ "SecurePassword123!", "AnotherPass456@", "ComplexP@ssw0rd!", } for _, password := range validPasswords { err := service.ValidatePassword(password) assert.NoError(t, err, "Password %s should be valid", password) } } func TestPasswordService_ValidatePassword_Weak(t *testing.T) { service, _, _ := setupTestPasswordServiceIntegration(t) weakPasswords := []string{ "short", "12345678", "password", "PASSWORD", } for _, password := range weakPasswords { err := service.ValidatePassword(password) assert.Error(t, err, "Password %s should be invalid", password) assert.Contains(t, err.Error(), "weak password") } } func TestPasswordService_ChangePassword_Success(t *testing.T) { // INT-04: Skip - requires PostgreSQL; service uses NOW() in queries, SQLite incompatible. t.Skip("requires PostgreSQL NOW() function") } func TestPasswordService_ChangePassword_WrongOldPassword(t *testing.T) { service, _, testDB := setupTestPasswordServiceIntegration(t) userID := uuid.New() oldPassword := "OldPassword123!" oldHash, _ := bcrypt.GenerateFromPassword([]byte(oldPassword), 12) createTestUser(t, testDB, userID, "test@example.com", "testuser", string(oldHash)) err := service.ChangePassword(userID, "WrongPassword123!", "NewPassword123!") assert.Error(t, err) assert.Contains(t, err.Error(), "incorrect old password") } func TestPasswordService_ChangePassword_UserNotFound(t *testing.T) { service, _, _ := setupTestPasswordServiceIntegration(t) userID := uuid.New() err := service.ChangePassword(userID, "OldPassword123!", "NewPassword123!") assert.Error(t, err) assert.Contains(t, err.Error(), "user not found") } func TestPasswordService_ChangePassword_WeakNewPassword(t *testing.T) { service, _, testDB := setupTestPasswordServiceIntegration(t) userID := uuid.New() oldPassword := "OldPassword123!" oldHash, _ := bcrypt.GenerateFromPassword([]byte(oldPassword), 12) createTestUser(t, testDB, userID, "test@example.com", "testuser", string(oldHash)) err := service.ChangePassword(userID, oldPassword, "weak") assert.Error(t, err) assert.Contains(t, err.Error(), "weak password") } func TestPasswordService_UpdatePassword_Success(t *testing.T) { // INT-04: Skip - requires PostgreSQL; service uses NOW() in queries, SQLite incompatible. t.Skip("requires PostgreSQL NOW() function") } func TestPasswordService_UpdatePassword_WeakPassword(t *testing.T) { service, _, testDB := setupTestPasswordServiceIntegration(t) userID := uuid.New() oldHash, _ := bcrypt.GenerateFromPassword([]byte("oldpassword"), 12) createTestUser(t, testDB, userID, "test@example.com", "testuser", string(oldHash)) err := service.UpdatePassword(userID, "weak") assert.Error(t, err) assert.Contains(t, err.Error(), "weak password") }