veza/veza-backend-api/internal/services/password_reset_service_test.go

391 lines
13 KiB
Go

package services
import (
"database/sql"
"testing"
"time"
"unsafe"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"veza-backend-api/internal/database"
"veza-backend-api/internal/models"
)
// setupTestPasswordResetService crée un PasswordResetService de test avec une base de données en mémoire
func setupTestPasswordResetService(t *testing.T) (*PasswordResetService, *database.Database, *gorm.DB) {
// Créer une base de données GORM en mémoire
gormDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err, "Failed to open test database")
// Auto-migrate pour créer la table users
err = gormDB.AutoMigrate(&models.User{})
require.NoError(t, err, "Failed to migrate users table")
// Créer la table password_reset_tokens manuellement
err = gormDB.Exec(`
CREATE TABLE password_reset_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token TEXT NOT NULL UNIQUE,
expires_at TIMESTAMP NOT NULL,
used INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)
`).Error
require.NoError(t, err, "Failed to create password_reset_tokens table")
// Créer les index
err = gormDB.Exec("CREATE INDEX idx_password_reset_tokens_token ON password_reset_tokens(token)").Error
require.NoError(t, err)
err = gormDB.Exec("CREATE INDEX idx_password_reset_tokens_user_id ON password_reset_tokens(user_id)").Error
require.NoError(t, err)
err = gormDB.Exec("CREATE INDEX idx_password_reset_tokens_expires_at ON password_reset_tokens(expires_at)").Error
require.NoError(t, err)
// Créer un utilisateur de test
user := &models.User{
Email: "test@example.com",
Username: "testuser",
Role: "user",
IsActive: true,
}
err = gormDB.Create(user).Error
require.NoError(t, err, "Failed to create test user")
// Obtenir le sql.DB depuis GORM
sqlDB, err := gormDB.DB()
require.NoError(t, err, "Failed to get sql.DB from GORM")
// Créer un Database wrapper en utilisant la même approche que createTestDatabase
// database.Database embeds *sql.DB, donc on utilise une structure temporaire avec le même layout
type tempDB struct {
*sql.DB
gormDB interface{}
config interface{}
logger interface{}
}
temp := &tempDB{DB: sqlDB}
testDB := (*database.Database)(unsafe.Pointer(temp))
// Créer le logger
logger, _ := zap.NewDevelopment()
// Créer le service
service := NewPasswordResetService(testDB, logger)
return service, testDB, gormDB
}
// TestPasswordResetService_GenerateToken teste la génération de token
func TestPasswordResetService_GenerateToken(t *testing.T) {
service, _, _ := setupTestPasswordResetService(t)
// Générer un token
token, err := service.GenerateToken()
assert.NoError(t, err)
assert.NotEmpty(t, token)
assert.Greater(t, len(token), 20, "Token should be at least 20 characters")
}
// TestPasswordResetService_GenerateToken_Unique teste que les tokens générés sont uniques
func TestPasswordResetService_GenerateToken_Unique(t *testing.T) {
service, _, _ := setupTestPasswordResetService(t)
// Générer plusieurs tokens
token1, err1 := service.GenerateToken()
token2, err2 := service.GenerateToken()
token3, err3 := service.GenerateToken()
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.NoError(t, err3)
// Vérifier que les tokens sont différents
assert.NotEqual(t, token1, token2)
assert.NotEqual(t, token2, token3)
assert.NotEqual(t, token1, token3)
}
// TestPasswordResetService_StoreToken teste le stockage d'un token
func TestPasswordResetService_StoreToken(t *testing.T) {
service, _, gormDB := setupTestPasswordResetService(t)
// Récupérer l'utilisateur
var user models.User
err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
require.NoError(t, err)
// Générer et stocker un token
token, err := service.GenerateToken()
require.NoError(t, err)
err = service.StoreToken(user.ID, token)
assert.NoError(t, err)
// Vérifier que le token a été stocké
var count int64
err = gormDB.Raw("SELECT COUNT(*) FROM password_reset_tokens WHERE token = ? AND user_id = ?", token, user.ID).Scan(&count).Error
require.NoError(t, err)
assert.Equal(t, int64(1), count, "Token should be stored")
}
// TestPasswordResetService_StoreToken_Expiration teste que le token a une expiration de 1h
func TestPasswordResetService_StoreToken_Expiration(t *testing.T) {
service, _, gormDB := setupTestPasswordResetService(t)
// Récupérer l'utilisateur
var user models.User
err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
require.NoError(t, err)
// Générer et stocker un token
token, err := service.GenerateToken()
require.NoError(t, err)
err = service.StoreToken(user.ID, token)
require.NoError(t, err)
// Vérifier l'expiration
var expiresAt time.Time
err = gormDB.Raw("SELECT expires_at FROM password_reset_tokens WHERE token = ?", token).Scan(&expiresAt).Error
require.NoError(t, err)
// L'expiration devrait être environ 1h dans le futur (avec une marge de 5 secondes)
expectedExpiry := time.Now().Add(1 * time.Hour)
assert.WithinDuration(t, expectedExpiry, expiresAt, 5*time.Second, "Token should expire in 1 hour")
}
// TestPasswordResetService_VerifyToken_Valid teste la vérification d'un token valide
func TestPasswordResetService_VerifyToken_Valid(t *testing.T) {
service, _, gormDB := setupTestPasswordResetService(t)
// Récupérer l'utilisateur
var user models.User
err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
require.NoError(t, err)
// Générer et stocker un token
token, err := service.GenerateToken()
require.NoError(t, err)
err = service.StoreToken(user.ID, token)
require.NoError(t, err)
// Vérifier le token
userID, err := service.VerifyToken(token)
assert.NoError(t, err)
assert.Equal(t, user.ID, userID, "User ID should match")
}
// TestPasswordResetService_VerifyToken_Invalid teste la vérification d'un token invalide
func TestPasswordResetService_VerifyToken_Invalid(t *testing.T) {
service, _, _ := setupTestPasswordResetService(t)
// Tenter de vérifier un token inexistant
userID, err := service.VerifyToken("invalid-token-123")
assert.Error(t, err)
assert.Equal(t, uuid.Nil, userID)
assert.Contains(t, err.Error(), "invalid token")
}
// TestPasswordResetService_VerifyToken_Expired teste la vérification d'un token expiré
func TestPasswordResetService_VerifyToken_Expired(t *testing.T) {
service, _, gormDB := setupTestPasswordResetService(t)
// Récupérer l'utilisateur
var user models.User
err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
require.NoError(t, err)
// Créer un token expiré manuellement
expiredTime := time.Now().Add(-2 * time.Hour)
token := "expired-token-123"
err = gormDB.Exec(`
INSERT INTO password_reset_tokens (user_id, token, expires_at, used, created_at)
VALUES (?, ?, ?, ?, ?)
`, user.ID, token, expiredTime, false, time.Now().Add(-3*time.Hour)).Error
require.NoError(t, err)
// Tenter de vérifier le token expiré
userID, err := service.VerifyToken(token)
assert.Error(t, err)
assert.Equal(t, uuid.Nil, userID)
assert.Contains(t, err.Error(), "expired")
}
// TestPasswordResetService_VerifyToken_AlreadyUsed teste la vérification d'un token déjà utilisé
func TestPasswordResetService_VerifyToken_AlreadyUsed(t *testing.T) {
service, _, gormDB := setupTestPasswordResetService(t)
// Récupérer l'utilisateur
var user models.User
err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
require.NoError(t, err)
// Créer un token déjà utilisé
expiresAt := time.Now().Add(1 * time.Hour)
token := "used-token-123"
err = gormDB.Exec(`
INSERT INTO password_reset_tokens (user_id, token, expires_at, used, created_at)
VALUES (?, ?, ?, ?, ?)
`, user.ID, token, expiresAt, true, time.Now()).Error
require.NoError(t, err)
// Tenter de vérifier le token utilisé
userID, err := service.VerifyToken(token)
assert.Error(t, err)
assert.Equal(t, uuid.Nil, userID)
assert.Contains(t, err.Error(), "already used")
}
// TestPasswordResetService_MarkTokenAsUsed teste le marquage d'un token comme utilisé
func TestPasswordResetService_MarkTokenAsUsed(t *testing.T) {
service, _, gormDB := setupTestPasswordResetService(t)
// Récupérer l'utilisateur
var user models.User
err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
require.NoError(t, err)
// Générer et stocker un token
token, err := service.GenerateToken()
require.NoError(t, err)
err = service.StoreToken(user.ID, token)
require.NoError(t, err)
// Marquer le token comme utilisé
err = service.MarkTokenAsUsed(token)
assert.NoError(t, err)
// Vérifier que le token est marqué comme utilisé
var used bool
err = gormDB.Raw("SELECT used FROM password_reset_tokens WHERE token = ?", token).Scan(&used).Error
require.NoError(t, err)
assert.True(t, used, "Token should be marked as used")
}
// TestPasswordResetService_MarkTokenAsUsed_InvalidToken teste le marquage d'un token inexistant
func TestPasswordResetService_MarkTokenAsUsed_InvalidToken(t *testing.T) {
service, _, _ := setupTestPasswordResetService(t)
// Tenter de marquer un token inexistant comme utilisé
err := service.MarkTokenAsUsed("non-existent-token")
assert.Error(t, err)
assert.Contains(t, err.Error(), "token not found")
}
// TestPasswordResetService_InvalidateOldTokens teste l'invalidation des anciens tokens
func TestPasswordResetService_InvalidateOldTokens(t *testing.T) {
service, _, gormDB := setupTestPasswordResetService(t)
// Récupérer l'utilisateur
var user models.User
err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
require.NoError(t, err)
// Créer plusieurs tokens non utilisés
expiresAt := time.Now().Add(1 * time.Hour)
token1 := "old-token-1"
token2 := "old-token-2"
token3 := "old-token-3"
err = gormDB.Exec(`
INSERT INTO password_reset_tokens (user_id, token, expires_at, used, created_at)
VALUES (?, ?, ?, ?, ?)
`, user.ID, token1, expiresAt, false, time.Now()).Error
require.NoError(t, err)
err = gormDB.Exec(`
INSERT INTO password_reset_tokens (user_id, token, expires_at, used, created_at)
VALUES (?, ?, ?, ?, ?)
`, user.ID, token2, expiresAt, false, time.Now()).Error
require.NoError(t, err)
err = gormDB.Exec(`
INSERT INTO password_reset_tokens (user_id, token, expires_at, used, created_at)
VALUES (?, ?, ?, ?, ?)
`, user.ID, token3, expiresAt, false, time.Now()).Error
require.NoError(t, err)
// Invalider les anciens tokens
err = service.InvalidateOldTokens(user.ID)
assert.NoError(t, err)
// Vérifier que tous les tokens sont marqués comme utilisés
var count int64
err = gormDB.Raw("SELECT COUNT(*) FROM password_reset_tokens WHERE user_id = ? AND used = FALSE", user.ID).Scan(&count).Error
require.NoError(t, err)
assert.Equal(t, int64(0), count, "All tokens should be invalidated")
}
// TestPasswordResetService_InvalidateOldTokens_OnlyUnused teste que seuls les tokens non utilisés sont invalidés
func TestPasswordResetService_InvalidateOldTokens_OnlyUnused(t *testing.T) {
service, _, gormDB := setupTestPasswordResetService(t)
// Récupérer l'utilisateur
var user models.User
err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
require.NoError(t, err)
// Créer un token utilisé et un token non utilisé
expiresAt := time.Now().Add(1 * time.Hour)
tokenUsed := "used-token"
tokenUnused := "unused-token"
err = gormDB.Exec(`
INSERT INTO password_reset_tokens (user_id, token, expires_at, used, created_at)
VALUES (?, ?, ?, ?, ?)
`, user.ID, tokenUsed, expiresAt, true, time.Now()).Error
require.NoError(t, err)
err = gormDB.Exec(`
INSERT INTO password_reset_tokens (user_id, token, expires_at, used, created_at)
VALUES (?, ?, ?, ?, ?)
`, user.ID, tokenUnused, expiresAt, false, time.Now()).Error
require.NoError(t, err)
// Invalider les anciens tokens
err = service.InvalidateOldTokens(user.ID)
assert.NoError(t, err)
// Vérifier que le token utilisé reste utilisé et l'autre est invalidé
var used1, used2 bool
err = gormDB.Raw("SELECT used FROM password_reset_tokens WHERE token = ?", tokenUsed).Scan(&used1).Error
require.NoError(t, err)
err = gormDB.Raw("SELECT used FROM password_reset_tokens WHERE token = ?", tokenUnused).Scan(&used2).Error
require.NoError(t, err)
assert.True(t, used1, "Used token should remain used")
assert.True(t, used2, "Unused token should be invalidated")
}
// TestPasswordResetService_StoreToken_Duplicate teste qu'on ne peut pas stocker deux tokens identiques
func TestPasswordResetService_StoreToken_Duplicate(t *testing.T) {
service, _, gormDB := setupTestPasswordResetService(t)
// Récupérer l'utilisateur
var user models.User
err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
require.NoError(t, err)
// Stocker un token
token := "duplicate-token"
err = service.StoreToken(user.ID, token)
require.NoError(t, err)
// Tenter de stocker le même token à nouveau
err = service.StoreToken(user.ID, token)
assert.Error(t, err, "Should not be able to store duplicate token")
}