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