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") }