- 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)
331 lines
10 KiB
Go
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")
|
|
}
|