247 lines
8.1 KiB
Go
247 lines
8.1 KiB
Go
package transactions
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap/zaptest"
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/gorm"
|
|
"veza-backend-api/internal/database"
|
|
"veza-backend-api/internal/models"
|
|
"veza-backend-api/internal/services"
|
|
"veza-backend-api/internal/testutils"
|
|
)
|
|
|
|
// setupTestDB crée une DB de test avec testcontainers
|
|
func setupTestDB(t *testing.T) *gorm.DB {
|
|
ctx := context.Background()
|
|
dsn, err := testutils.GetTestContainerDB(ctx)
|
|
require.NoError(t, err, "Failed to setup test database")
|
|
|
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
|
require.NoError(t, err, "Failed to open database connection")
|
|
|
|
// Auto-migrate models nécessaires
|
|
err = db.AutoMigrate(
|
|
&models.User{},
|
|
&models.Role{},
|
|
&models.UserRole{},
|
|
)
|
|
require.NoError(t, err, "Failed to migrate database")
|
|
|
|
return db
|
|
}
|
|
|
|
// cleanupTestDB nettoie la DB entre les tests
|
|
func cleanupTestDB(t *testing.T, db *gorm.DB) {
|
|
// Supprimer toutes les données
|
|
db.Exec("TRUNCATE TABLE user_roles CASCADE")
|
|
db.Exec("TRUNCATE TABLE users CASCADE")
|
|
db.Exec("TRUNCATE TABLE roles CASCADE")
|
|
}
|
|
|
|
// createTestUser crée un utilisateur de test
|
|
func createTestUser(t *testing.T, db *gorm.DB) *models.User {
|
|
user := &models.User{
|
|
Username: "testuser_" + uuid.New().String()[:8],
|
|
Slug: "testuser_" + uuid.New().String()[:8], // Unique slug
|
|
Email: "test_" + uuid.New().String()[:8] + "@example.com",
|
|
PasswordHash: "$2a$10$examplehash",
|
|
IsActive: true,
|
|
IsVerified: true,
|
|
}
|
|
err := db.Create(user).Error
|
|
require.NoError(t, err)
|
|
return user
|
|
}
|
|
|
|
// createTestRole crée un rôle de test
|
|
func createTestRole(t *testing.T, db *gorm.DB) *models.Role {
|
|
role := &models.Role{
|
|
Name: "test_role_" + uuid.New().String()[:8],
|
|
Description: "Test role for transaction tests",
|
|
}
|
|
err := db.Create(role).Error
|
|
require.NoError(t, err)
|
|
return role
|
|
}
|
|
|
|
// TestAssignRoleToUser_Success vérifie que l'assignation fonctionne correctement
|
|
func TestAssignRoleToUser_Success(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
defer cleanupTestDB(t, db)
|
|
|
|
logger := zaptest.NewLogger(t)
|
|
// Initialize RBAC service
|
|
dbWrapper := &database.Database{GormDB: db}
|
|
rbacService := services.NewRBACService(dbWrapper, logger)
|
|
|
|
user := createTestUser(t, db)
|
|
role := createTestRole(t, db)
|
|
|
|
// Assigner le rôle
|
|
err := rbacService.AssignRoleToUser(context.Background(), user.ID, role.ID)
|
|
require.NoError(t, err, "AssignRoleToUser should succeed")
|
|
|
|
// Vérifier que l'assignation existe
|
|
var count int64
|
|
db.Model(&models.UserRole{}).
|
|
Where("user_id = ? AND role_id = ?", user.ID, role.ID).
|
|
Count(&count)
|
|
assert.Equal(t, int64(1), count, "UserRole should be created")
|
|
}
|
|
|
|
// TestAssignRoleToUser_RollbackOnUserNotFound vérifie le rollback si l'utilisateur n'existe pas
|
|
func TestAssignRoleToUser_RollbackOnUserNotFound(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
defer cleanupTestDB(t, db)
|
|
|
|
logger := zaptest.NewLogger(t)
|
|
rbacService := services.NewRBACService(&database.Database{GormDB: db}, logger)
|
|
|
|
role := createTestRole(t, db)
|
|
fakeUserID := uuid.New()
|
|
|
|
// Tenter d'assigner le rôle à un utilisateur inexistant
|
|
err := rbacService.AssignRoleToUser(context.Background(), fakeUserID, role.ID)
|
|
require.Error(t, err, "AssignRoleToUser should fail")
|
|
assert.Contains(t, err.Error(), "user not found", "Error should mention user not found")
|
|
|
|
// Vérifier qu'aucune assignation n'a été créée
|
|
var count int64
|
|
db.Model(&models.UserRole{}).Count(&count)
|
|
assert.Equal(t, int64(0), count, "No UserRole should be created on error")
|
|
}
|
|
|
|
// TestAssignRoleToUser_RollbackOnRoleNotFound vérifie le rollback si le rôle n'existe pas
|
|
func TestAssignRoleToUser_RollbackOnRoleNotFound(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
defer cleanupTestDB(t, db)
|
|
|
|
logger := zaptest.NewLogger(t)
|
|
rbacService := services.NewRBACService(&database.Database{GormDB: db}, logger)
|
|
|
|
user := createTestUser(t, db)
|
|
fakeRoleID := uuid.New()
|
|
|
|
// Tenter d'assigner un rôle inexistant
|
|
err := rbacService.AssignRoleToUser(context.Background(), user.ID, fakeRoleID)
|
|
require.Error(t, err, "AssignRoleToUser should fail")
|
|
assert.Contains(t, err.Error(), "role not found", "Error should mention role not found")
|
|
|
|
// Vérifier qu'aucune assignation n'a été créée
|
|
var count int64
|
|
db.Model(&models.UserRole{}).Count(&count)
|
|
assert.Equal(t, int64(0), count, "No UserRole should be created on error")
|
|
}
|
|
|
|
// TestAssignRoleToUser_RollbackOnDuplicate vérifie le rollback si le rôle est déjà assigné
|
|
func TestAssignRoleToUser_RollbackOnDuplicate(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
defer cleanupTestDB(t, db)
|
|
|
|
logger := zaptest.NewLogger(t)
|
|
rbacService := services.NewRBACService(&database.Database{GormDB: db}, logger)
|
|
|
|
user := createTestUser(t, db)
|
|
role := createTestRole(t, db)
|
|
|
|
// Première assignation (succès)
|
|
err := rbacService.AssignRoleToUser(context.Background(), user.ID, role.ID)
|
|
require.NoError(t, err, "First assignment should succeed")
|
|
|
|
// Deuxième assignation (doublon)
|
|
err = rbacService.AssignRoleToUser(context.Background(), user.ID, role.ID)
|
|
require.Error(t, err, "Second assignment should fail")
|
|
assert.Contains(t, err.Error(), "role already assigned", "Error should mention duplicate")
|
|
|
|
// Vérifier qu'il n'y a qu'une seule assignation
|
|
var count int64
|
|
db.Model(&models.UserRole{}).
|
|
Where("user_id = ? AND role_id = ?", user.ID, role.ID).
|
|
Count(&count)
|
|
assert.Equal(t, int64(1), count, "Should have exactly one UserRole")
|
|
}
|
|
|
|
// TestAssignRoleToUser_Concurrency vérifie qu'il n'y a pas de race condition
|
|
func TestAssignRoleToUser_Concurrency(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
defer cleanupTestDB(t, db)
|
|
|
|
logger := zaptest.NewLogger(t)
|
|
rbacService := services.NewRBACService(&database.Database{GormDB: db}, logger)
|
|
|
|
user := createTestUser(t, db)
|
|
role := createTestRole(t, db)
|
|
|
|
// Lancer 10 goroutines qui tentent d'assigner le même rôle simultanément
|
|
results := make(chan error, 10)
|
|
for i := 0; i < 10; i++ {
|
|
go func() {
|
|
err := rbacService.AssignRoleToUser(context.Background(), user.ID, role.ID)
|
|
results <- err
|
|
}()
|
|
}
|
|
|
|
// Collecter les résultats
|
|
successCount := 0
|
|
errorCount := 0
|
|
for i := 0; i < 10; i++ {
|
|
err := <-results
|
|
if err == nil {
|
|
successCount++
|
|
} else {
|
|
errorCount++
|
|
assert.Contains(t, err.Error(), "role already assigned", "Error should be about duplicate")
|
|
}
|
|
}
|
|
|
|
// Une seule assignation devrait réussir
|
|
assert.Equal(t, 1, successCount, "Only one assignment should succeed")
|
|
assert.Equal(t, 9, errorCount, "Nine assignments should fail due to duplicate")
|
|
|
|
// Vérifier qu'il n'y a qu'une seule assignation en DB
|
|
var count int64
|
|
db.Model(&models.UserRole{}).
|
|
Where("user_id = ? AND role_id = ?", user.ID, role.ID).
|
|
Count(&count)
|
|
assert.Equal(t, int64(1), count, "Should have exactly one UserRole despite concurrent attempts")
|
|
}
|
|
|
|
// TestAssignRoleToUser_Atomicity vérifie l'atomicité complète de la transaction
|
|
func TestAssignRoleToUser_Atomicity(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
defer cleanupTestDB(t, db)
|
|
|
|
logger := zaptest.NewLogger(t)
|
|
rbacService := services.NewRBACService(&database.Database{GormDB: db}, logger)
|
|
|
|
user := createTestUser(t, db)
|
|
role := createTestRole(t, db)
|
|
|
|
// Supprimer le rôle juste avant l'assignation pour forcer une erreur
|
|
// (simulation d'une erreur au milieu de la transaction)
|
|
// Note: Dans une vraie transaction, cela ne devrait pas arriver car FOR UPDATE verrouille
|
|
// Mais on peut tester en supprimant le rôle après le début de la transaction
|
|
|
|
// Créer un hook GORM pour simuler une erreur
|
|
// Pour ce test, on va simplement vérifier que si le rôle est supprimé
|
|
// entre la vérification et l'INSERT, la contrainte FK bloque l'insertion
|
|
|
|
// Assigner le rôle normalement d'abord
|
|
err := rbacService.AssignRoleToUser(context.Background(), user.ID, role.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Supprimer le rôle
|
|
db.Delete(role)
|
|
|
|
// Tenter d'assigner à un autre utilisateur (devrait échouer car le rôle n'existe plus)
|
|
user2 := createTestUser(t, db)
|
|
err = rbacService.AssignRoleToUser(context.Background(), user2.ID, role.ID)
|
|
require.Error(t, err, "Should fail because role was deleted")
|
|
assert.Contains(t, err.Error(), "role not found", "Error should mention role not found")
|
|
}
|