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