veza/veza-backend-api/internal/services/password_service_integration_test.go
senke 51984e9a1f
Some checks failed
Backend API CI / test-unit (push) Failing after 0s
Backend API CI / test-integration (push) Failing after 0s
feat(security): v0.901 Ironclad - fix 5 critical/high vulnerabilities
- OAuth: use JWTService+SessionService, httpOnly cookies (VEZA-SEC-001)
- Remove PasswordService.GenerateJWT (VEZA-SEC-002)
- Hyperswitch webhook: mandatory verification, 500 if secret empty (VEZA-SEC-005)
- Auth middleware: TokenBlacklist.IsBlacklisted check (VEZA-SEC-006)
- Waveform: ValidateExecPath before exec (VEZA-SEC-007)
2026-02-26 19:34:45 +01:00

331 lines
10 KiB
Go

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()
token, _, err := service.GeneratePasswordResetToken(userID)
assert.NoError(t, err)
// Verify token is stored in database
ctx := context.Background()
var storedToken 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(&storedToken, &storedExpiresAt, &storedUsed)
assert.NoError(t, err)
assert.Equal(t, token, storedToken)
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
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(), "expired_token", expiredTime, false)
require.NoError(t, err)
err = service.ResetPassword("expired_token", "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")
}