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

381 lines
12 KiB
Go

package services
import (
"database/sql"
"testing"
"time"
"unsafe"
"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"
)
// setupTestEmailVerificationService crée un EmailVerificationService de test avec une base de données en mémoire
func setupTestEmailVerificationService(t *testing.T) (*EmailVerificationService, *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 email_verification_tokens manuellement
err = gormDB.Exec(`
CREATE TABLE email_verification_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 email_verification_tokens table")
// Créer les index
err = gormDB.Exec("CREATE INDEX idx_email_verification_tokens_token ON email_verification_tokens(token)").Error
require.NoError(t, err)
err = gormDB.Exec("CREATE INDEX idx_email_verification_tokens_user_id ON email_verification_tokens(user_id)").Error
require.NoError(t, err)
err = gormDB.Exec("CREATE INDEX idx_email_verification_tokens_expires_at ON email_verification_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 *sql.DB depuis GORM
sqlDB, err := gormDB.DB()
require.NoError(t, err, "Failed to get sql.DB from GORM")
// Créer un database.Database de test
// database.Database embeds *sql.DB directement, donc on crée une structure vide puis on assigne
// Pour les tests, on crée un Database avec sqlDB embedé
// Note: On ne peut pas initialiser un type embedé dans un struct literal, donc on utilise une approche alternative
// On crée un Database avec les champs nécessaires
testDB := &database.Database{}
// On assigne sqlDB via un type composite qui contient *sql.DB
// Mais comme *sql.DB est embedé, on doit utiliser une approche différente
// Solution: créer un Database temporaire pour obtenir la structure, puis copier sqlDB
// Ou utiliser une fonction helper qui crée un Database avec sqlDB
// Pour les tests, on va créer un Database minimal en utilisant reflection ou une fonction helper
// Mais la solution la plus simple: utiliser directement sqlDB dans les tests via un wrapper
// Créons un Database avec sqlDB assigné manuellement via une fonction helper de test
testDB = createTestDatabase(sqlDB)
// Créer le logger
logger, _ := zap.NewDevelopment()
// Créer le service
service := NewEmailVerificationService(testDB, logger)
return service, testDB, gormDB
}
// createTestDatabase crée un database.Database de test avec un *sql.DB
// database.Database embeds *sql.DB, donc on utilise une structure temporaire avec le même layout
func createTestDatabase(sqlDB *sql.DB) *database.Database {
// Créer une structure temporaire avec le même layout que database.Database
// database.Database a *sql.DB en premier (embedé), puis GormDB, config, logger
type tempDB struct {
*sql.DB
gormDB *gorm.DB
config interface{}
logger interface{}
}
// Créer la structure temporaire avec sqlDB
temp := &tempDB{
DB: sqlDB,
}
// Convertir en database.Database en utilisant unsafe.Pointer
// Note: Cette conversion est sûre car les deux structures ont *sql.DB en premier
// et on n'utilise que les méthodes de *sql.DB dans les tests
return (*database.Database)(unsafe.Pointer(temp))
}
func TestEmailVerificationService_GenerateToken(t *testing.T) {
logger, _ := zap.NewDevelopment()
service := &EmailVerificationService{
db: nil, // Pas besoin pour GenerateToken
logger: logger,
}
token, err := service.GenerateToken()
assert.NoError(t, err)
assert.NotEmpty(t, token)
assert.GreaterOrEqual(t, len(token), 32) // base64 URL encoding de 32 bytes donne ~43 caractères
}
func TestEmailVerificationService_GenerateToken_Unique(t *testing.T) {
logger, _ := zap.NewDevelopment()
service := &EmailVerificationService{
db: nil,
logger: logger,
}
token1, err1 := service.GenerateToken()
require.NoError(t, err1)
token2, err2 := service.GenerateToken()
require.NoError(t, err2)
assert.NotEqual(t, token1, token2, "Tokens should be unique")
}
func TestEmailVerificationService_StoreToken(t *testing.T) {
service, _, gormDB := setupTestEmailVerificationService(t)
var user models.User
err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
require.NoError(t, err)
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
sqlDB, _ := gormDB.DB()
err = sqlDB.QueryRow("SELECT COUNT(*) FROM email_verification_tokens WHERE user_id = ? AND token = ?", user.ID, token).Scan(&count)
assert.NoError(t, err)
assert.Equal(t, int64(1), count)
}
func TestEmailVerificationService_StoreToken_Expiration(t *testing.T) {
service, _, gormDB := setupTestEmailVerificationService(t)
var user models.User
err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
require.NoError(t, err)
token, err := service.GenerateToken()
require.NoError(t, err)
err = service.StoreToken(user.ID, token)
require.NoError(t, err)
// Vérifier que l'expiration est dans 24h (avec une marge de ±1 minute)
var expiresAt time.Time
sqlDB, _ := gormDB.DB()
err = sqlDB.QueryRow("SELECT expires_at FROM email_verification_tokens WHERE token = ?", token).Scan(&expiresAt)
assert.NoError(t, err)
expectedExpiration := time.Now().Add(24 * time.Hour)
diff := expiresAt.Sub(expectedExpiration)
assert.True(t, diff < time.Minute && diff > -time.Minute, "Expiration should be approximately 24h from now")
}
func TestEmailVerificationService_VerifyToken_ValidToken(t *testing.T) {
service, _, gormDB := setupTestEmailVerificationService(t)
var user models.User
err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
require.NoError(t, err)
token, err := service.GenerateToken()
require.NoError(t, err)
err = service.StoreToken(user.ID, token)
require.NoError(t, err)
userID, err := service.VerifyToken(token)
assert.NoError(t, err)
assert.Equal(t, user.ID, userID)
// Vérifier que le token a été marqué comme utilisé
var used bool
sqlDB, _ := gormDB.DB()
err = sqlDB.QueryRow("SELECT used FROM email_verification_tokens WHERE token = ?", token).Scan(&used)
assert.NoError(t, err)
assert.True(t, used)
}
func TestEmailVerificationService_VerifyToken_InvalidToken(t *testing.T) {
service, _, _ := setupTestEmailVerificationService(t)
invalidToken := "invalid-token-123"
userID, err := service.VerifyToken(invalidToken)
assert.Error(t, err)
assert.Equal(t, int64(0), userID)
assert.Contains(t, err.Error(), "invalid token")
}
func TestEmailVerificationService_VerifyToken_ExpiredToken(t *testing.T) {
service, _, gormDB := setupTestEmailVerificationService(t)
var user models.User
err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
require.NoError(t, err)
token, err := service.GenerateToken()
require.NoError(t, err)
// Insérer un token expiré directement
sqlDB, _ := gormDB.DB()
expiredAt := time.Now().Add(-1 * time.Hour) // Expiré il y a 1 heure
_, err = sqlDB.Exec(
"INSERT INTO email_verification_tokens (user_id, token, expires_at, used) VALUES (?, ?, ?, 0)",
user.ID, token, expiredAt,
)
require.NoError(t, err)
userID, err := service.VerifyToken(token)
assert.Error(t, err)
assert.Equal(t, int64(0), userID)
assert.Contains(t, err.Error(), "token expired")
}
func TestEmailVerificationService_VerifyToken_AlreadyUsed(t *testing.T) {
service, _, gormDB := setupTestEmailVerificationService(t)
var user models.User
err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
require.NoError(t, err)
token, err := service.GenerateToken()
require.NoError(t, err)
// Insérer un token déjà utilisé
sqlDB, _ := gormDB.DB()
expiresAt := time.Now().Add(24 * time.Hour)
_, err = sqlDB.Exec(
"INSERT INTO email_verification_tokens (user_id, token, expires_at, used) VALUES (?, ?, ?, 1)",
user.ID, token, expiresAt,
)
require.NoError(t, err)
userID, err := service.VerifyToken(token)
assert.Error(t, err)
assert.Equal(t, int64(0), userID)
assert.Contains(t, err.Error(), "token already used")
}
func TestEmailVerificationService_VerifyToken_CannotReuse(t *testing.T) {
service, _, gormDB := setupTestEmailVerificationService(t)
var user models.User
err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
require.NoError(t, err)
token, err := service.GenerateToken()
require.NoError(t, err)
err = service.StoreToken(user.ID, token)
require.NoError(t, err)
// Première vérification - devrait réussir
userID, err := service.VerifyToken(token)
assert.NoError(t, err)
assert.Equal(t, user.ID, userID)
// Deuxième vérification - devrait échouer car déjà utilisé
userID2, err2 := service.VerifyToken(token)
assert.Error(t, err2)
assert.Equal(t, int64(0), userID2)
assert.Contains(t, err2.Error(), "token already used")
}
func TestEmailVerificationService_InvalidateOldTokens(t *testing.T) {
service, _, gormDB := setupTestEmailVerificationService(t)
var user models.User
err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
require.NoError(t, err)
// Créer plusieurs tokens pour le même utilisateur
token1, err := service.GenerateToken()
require.NoError(t, err)
err = service.StoreToken(user.ID, token1)
require.NoError(t, err)
token2, err := service.GenerateToken()
require.NoError(t, err)
err = service.StoreToken(user.ID, token2)
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
sqlDB, _ := gormDB.DB()
var count int
err = sqlDB.QueryRow("SELECT COUNT(*) FROM email_verification_tokens WHERE user_id = ? AND used = 0", user.ID).Scan(&count)
assert.NoError(t, err)
assert.Equal(t, 0, count, "All tokens should be invalidated")
}
func TestEmailVerificationService_InvalidateOldTokens_NoTokens(t *testing.T) {
service, _, gormDB := setupTestEmailVerificationService(t)
var user models.User
err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
require.NoError(t, err)
// Invalider les tokens pour un utilisateur sans tokens
err = service.InvalidateOldTokens(user.ID)
assert.NoError(t, err) // Ne devrait pas retourner d'erreur même s'il n'y a pas de tokens
}
func TestEmailVerificationService_InvalidateOldTokens_MultipleUsers(t *testing.T) {
service, _, gormDB := setupTestEmailVerificationService(t)
// Créer un deuxième utilisateur
user2 := &models.User{
Email: "user2@example.com",
Username: "user2",
Role: "user",
IsActive: true,
}
err := gormDB.Create(user2).Error
require.NoError(t, err)
var user1 models.User
err = gormDB.Where("email = ?", "test@example.com").First(&user1).Error
require.NoError(t, err)
// Créer des tokens pour les deux utilisateurs
token1, err := service.GenerateToken()
require.NoError(t, err)
err = service.StoreToken(user1.ID, token1)
require.NoError(t, err)
token2, err := service.GenerateToken()
require.NoError(t, err)
err = service.StoreToken(user2.ID, token2)
require.NoError(t, err)
// Invalider uniquement les tokens de user1
err = service.InvalidateOldTokens(user1.ID)
assert.NoError(t, err)
// Vérifier que seul le token de user1 est invalidé
sqlDB, _ := gormDB.DB()
var count1 int
err = sqlDB.QueryRow("SELECT COUNT(*) FROM email_verification_tokens WHERE user_id = ? AND used = 0", user1.ID).Scan(&count1)
assert.NoError(t, err)
assert.Equal(t, 0, count1, "User1 tokens should be invalidated")
var count2 int
err = sqlDB.QueryRow("SELECT COUNT(*) FROM email_verification_tokens WHERE user_id = ? AND used = 0", user2.ID).Scan(&count2)
assert.NoError(t, err)
assert.Equal(t, 1, count2, "User2 tokens should not be invalidated")
}