391 lines
13 KiB
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")
|
|
}
|