403 lines
13 KiB
Go
403 lines
13 KiB
Go
package services
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"testing"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"veza-backend-api/internal/database"
|
|
"veza-backend-api/internal/models"
|
|
|
|
"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"
|
|
)
|
|
|
|
// 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 avec le schéma complet
|
|
// Correspond à migrations/010_auth_and_users.sql
|
|
err = gormDB.Exec(`
|
|
CREATE TABLE email_verification_tokens (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
token TEXT NOT NULL UNIQUE,
|
|
token_hash TEXT NOT NULL,
|
|
email TEXT NOT NULL,
|
|
verified INTEGER NOT NULL DEFAULT 0,
|
|
used INTEGER NOT NULL DEFAULT 0,
|
|
verified_at TIMESTAMP,
|
|
expires_at TIMESTAMP NOT NULL,
|
|
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_token_hash ON email_verification_tokens(token_hash)").Error
|
|
require.NoError(t, err)
|
|
err = gormDB.Exec("CREATE INDEX idx_email_verification_tokens_email ON email_verification_tokens(email)").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, user.Email, 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, user.Email, 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, user.Email, 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, uuid.Nil, userID)
|
|
assert.Contains(t, err.Error(), "invalid token")
|
|
}
|
|
|
|
// hashToken helper pour hasher le token (même logique que dans le service)
|
|
func hashTokenForTest(token string) string {
|
|
hash := sha256.Sum256([]byte(token))
|
|
return hex.EncodeToString(hash[:])
|
|
}
|
|
|
|
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)
|
|
tokenHash := hashTokenForTest(token)
|
|
|
|
// 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, email, token, token_hash, expires_at, used) VALUES (?, ?, ?, ?, ?, 0)",
|
|
user.ID, user.Email, token, tokenHash, expiredAt,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
userID, err := service.VerifyToken(token)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, uuid.Nil, 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)
|
|
tokenHash := hashTokenForTest(token)
|
|
|
|
// 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, email, token, token_hash, expires_at, used) VALUES (?, ?, ?, ?, ?, 1)",
|
|
user.ID, user.Email, token, tokenHash, expiresAt,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
userID, err := service.VerifyToken(token)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, uuid.Nil, 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, user.Email, 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, uuid.Nil, 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, user.Email, token1)
|
|
require.NoError(t, err)
|
|
|
|
token2, err := service.GenerateToken()
|
|
require.NoError(t, err)
|
|
err = service.StoreToken(user.ID, user.Email, 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, user1.Email, token1)
|
|
require.NoError(t, err)
|
|
|
|
token2, err := service.GenerateToken()
|
|
require.NoError(t, err)
|
|
err = service.StoreToken(user2.ID, user2.Email, 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")
|
|
}
|