Configure Maven repository for unionflow-server-api dependency

This commit is contained in:
dahoud
2025-12-10 01:08:17 +00:00
commit 4a0c5f9d33
320 changed files with 33373 additions and 0 deletions

View File

@@ -0,0 +1,369 @@
package dev.lions.unionflow.server.service;
import static org.assertj.core.api.Assertions.*;
import dev.lions.unionflow.server.api.dto.membre.MembreDTO;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.*;
/**
* Tests unitaires pour MembreImportExportService
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-19
*/
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class MembreImportExportServiceTest {
@Inject MembreImportExportService importExportService;
@Inject MembreRepository membreRepository;
@Inject OrganisationRepository organisationRepository;
@Inject MembreService membreService;
private Organisation testOrganisation;
private List<Membre> testMembres;
@BeforeEach
@Transactional
void setupTestData() {
// Créer une organisation de test
testOrganisation =
Organisation.builder()
.nom("Organisation Test Import/Export Service")
.typeOrganisation("ASSOCIATION")
.statut("ACTIF")
.email("org-service-" + System.currentTimeMillis() + "@test.com")
.build();
testOrganisation.setDateCreation(LocalDateTime.now());
testOrganisation.setActif(true);
organisationRepository.persist(testOrganisation);
// Créer quelques membres de test
testMembres = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
Membre membre =
Membre.builder()
.numeroMembre("UF-SERVICE-TEST-" + i)
.nom("NomService" + i)
.prenom("PrenomService" + i)
.email("service" + i + "-" + System.currentTimeMillis() + "@test.com")
.telephone("+2217012345" + (10 + i))
.dateNaissance(LocalDate.of(1985 + i, 1, 1))
.dateAdhesion(LocalDate.of(2022, 1, 1))
.organisation(testOrganisation)
.build();
membre.setDateCreation(LocalDateTime.now());
membre.setActif(true);
membreRepository.persist(membre);
testMembres.add(membre);
}
}
@AfterEach
@Transactional
void cleanupTestData() {
if (testMembres != null) {
testMembres.forEach(
membre -> {
if (membre.getId() != null) {
Membre m = membreRepository.findById(membre.getId());
if (m != null) {
membreRepository.delete(m);
}
}
});
}
if (testOrganisation != null && testOrganisation.getId() != null) {
Organisation org = organisationRepository.findById(testOrganisation.getId());
if (org != null) {
organisationRepository.delete(org);
}
}
}
@Test
@Order(1)
@DisplayName("Doit générer un modèle d'import Excel valide")
void testGenererModeleImport() throws Exception {
// When
byte[] modele = importExportService.genererModeleImport();
// Then
assertThat(modele).isNotNull();
assertThat(modele.length).isGreaterThan(0);
// Vérifier que c'est un fichier Excel valide
try (Workbook workbook = new XSSFWorkbook(new ByteArrayInputStream(modele))) {
Sheet sheet = workbook.getSheetAt(0);
assertThat(sheet).isNotNull();
Row headerRow = sheet.getRow(0);
assertThat(headerRow).isNotNull();
// Vérifier la présence de colonnes essentielles
boolean hasNom = false, hasPrenom = false, hasEmail = false;
for (Cell cell : headerRow) {
String value = cell.getStringCellValue().toLowerCase();
if (value.contains("nom")) hasNom = true;
if (value.contains("prenom")) hasPrenom = true;
if (value.contains("email")) hasEmail = true;
}
assertThat(hasNom).isTrue();
assertThat(hasPrenom).isTrue();
assertThat(hasEmail).isTrue();
}
}
@Test
@Order(2)
@DisplayName("Doit importer des membres depuis un fichier Excel valide")
void testImporterDepuisExcel() throws Exception {
// Given - Créer un fichier Excel de test
byte[] excelFile = createValidExcelFile();
ByteArrayInputStream inputStream = new ByteArrayInputStream(excelFile);
// When
MembreImportExportService.ResultatImport resultat =
importExportService.importerMembres(
inputStream,
"test_import.xlsx",
testOrganisation.getId(),
"ACTIF",
false,
false);
// Then
assertThat(resultat).isNotNull();
assertThat(resultat.lignesTraitees).isGreaterThan(0);
assertThat(resultat.membresImportes).isNotEmpty();
assertThat(resultat.erreurs).isEmpty();
}
@Test
@Order(3)
@DisplayName("Doit gérer les erreurs lors de l'import Excel")
void testImporterExcelAvecErreurs() throws Exception {
// Given - Créer un fichier Excel avec des données invalides
byte[] excelFile = createInvalidExcelFile();
ByteArrayInputStream inputStream = new ByteArrayInputStream(excelFile);
// When
MembreImportExportService.ResultatImport resultat =
importExportService.importerMembres(
inputStream,
"test_invalid.xlsx",
testOrganisation.getId(),
"ACTIF",
false,
true); // Ignorer les erreurs
// Then
assertThat(resultat).isNotNull();
assertThat(resultat.erreurs).isNotEmpty();
}
@Test
@Order(4)
@DisplayName("Doit exporter des membres vers Excel")
void testExporterVersExcel() throws Exception {
// Given - Convertir les membres de test en DTOs
List<MembreDTO> membresDTO = new ArrayList<>();
testMembres.forEach(m -> membresDTO.add(membreService.convertToDTO(m)));
// When
byte[] excelData =
importExportService.exporterVersExcel(
membresDTO,
List.of("nom", "prenom", "email", "telephone"),
true, // inclureHeaders
false, // formaterDates
false, // inclureStatistiques
null); // motDePasse
// Then
assertThat(excelData).isNotNull();
assertThat(excelData.length).isGreaterThan(0);
// Vérifier que c'est un fichier Excel valide
try (Workbook workbook = new XSSFWorkbook(new ByteArrayInputStream(excelData))) {
Sheet sheet = workbook.getSheetAt(0);
assertThat(sheet).isNotNull();
assertThat(sheet.getLastRowNum()).isGreaterThan(0);
}
}
@Test
@Order(5)
@DisplayName("Doit exporter des membres vers Excel avec statistiques")
void testExporterVersExcelAvecStatistiques() throws Exception {
// Given
List<MembreDTO> membresDTO = new ArrayList<>();
testMembres.forEach(m -> membresDTO.add(membreService.convertToDTO(m)));
// When
byte[] excelData =
importExportService.exporterVersExcel(
membresDTO,
List.of("nom", "prenom", "email"),
true, // inclureHeaders
false, // formaterDates
true, // inclureStatistiques
null); // motDePasse
// Then
assertThat(excelData).isNotNull();
// Vérifier qu'il y a plusieurs feuilles (données + statistiques)
try (Workbook workbook = new XSSFWorkbook(new ByteArrayInputStream(excelData))) {
assertThat(workbook.getNumberOfSheets()).isGreaterThan(1);
Sheet statsSheet = workbook.getSheet("Statistiques");
assertThat(statsSheet).isNotNull();
}
}
@Test
@Order(6)
@DisplayName("Doit exporter des membres vers Excel avec chiffrement")
void testExporterVersExcelAvecChiffrement() throws Exception {
// Given
List<MembreDTO> membresDTO = new ArrayList<>();
testMembres.forEach(m -> membresDTO.add(membreService.convertToDTO(m)));
// When
byte[] excelData =
importExportService.exporterVersExcel(
membresDTO,
List.of("nom", "prenom", "email"),
true, // inclureHeaders
false, // formaterDates
false, // inclureStatistiques
"testPassword123"); // motDePasse
// Then
assertThat(excelData).isNotNull();
// Note: La vérification du chiffrement nécessiterait d'essayer d'ouvrir le fichier avec le mot de passe
}
@Test
@Order(7)
@DisplayName("Doit exporter des membres vers CSV")
void testExporterVersCSV() throws Exception {
// Given
List<MembreDTO> membresDTO = new ArrayList<>();
testMembres.forEach(m -> membresDTO.add(membreService.convertToDTO(m)));
// When - Utiliser les groupes de colonnes attendus par la méthode
byte[] csvData =
importExportService.exporterVersCSV(
membresDTO,
List.of("PERSO", "CONTACT"), // Groupes de colonnes
true, // inclureHeaders
false); // formaterDates
// Then
assertThat(csvData).isNotNull();
assertThat(csvData.length).isGreaterThan(0);
// Vérifier le contenu CSV - les en-têtes sont en français avec majuscules
String csvContent = new String(csvData, java.nio.charset.StandardCharsets.UTF_8);
assertThat(csvContent).contains("Nom");
assertThat(csvContent).contains("Prénom");
assertThat(csvContent).contains("Email");
}
@Test
@Order(8)
@DisplayName("Doit rejeter un format de fichier non supporté")
void testFormatNonSupporte() {
// Given
byte[] invalidFile = "Ceci n'est pas un fichier Excel".getBytes();
ByteArrayInputStream inputStream = new ByteArrayInputStream(invalidFile);
// When & Then
assertThatThrownBy(
() ->
importExportService.importerMembres(
inputStream,
"test.txt",
testOrganisation.getId(),
"ACTIF",
false,
false))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Format de fichier non supporté");
}
/**
* Crée un fichier Excel valide pour les tests d'import
*/
private byte[] createValidExcelFile() throws Exception {
try (Workbook workbook = new XSSFWorkbook();
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
Sheet sheet = workbook.createSheet("Membres");
// En-têtes
Row headerRow = sheet.createRow(0);
String[] headers = {
"nom", "prenom", "email", "telephone", "dateNaissance", "dateAdhesion"
};
for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
}
// Données valides
Row dataRow = sheet.createRow(1);
dataRow.createCell(0).setCellValue("TestNom");
dataRow.createCell(1).setCellValue("TestPrenom");
dataRow.createCell(2).setCellValue("test-valid-" + System.currentTimeMillis() + "@test.com");
dataRow.createCell(3).setCellValue("+221701234999");
dataRow.createCell(4).setCellValue("1990-01-01");
dataRow.createCell(5).setCellValue("2023-01-01");
workbook.write(out);
return out.toByteArray();
}
}
/**
* Crée un fichier Excel avec des données invalides pour tester la gestion d'erreurs
*/
private byte[] createInvalidExcelFile() throws Exception {
try (Workbook workbook = new XSSFWorkbook();
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
Sheet sheet = workbook.createSheet("Membres");
// En-têtes
Row headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("nom");
headerRow.createCell(1).setCellValue("prenom");
headerRow.createCell(2).setCellValue("email");
// Données invalides (email manquant)
Row dataRow = sheet.createRow(1);
dataRow.createCell(0).setCellValue("TestNom");
dataRow.createCell(1).setCellValue("TestPrenom");
// Email manquant - devrait générer une erreur
workbook.write(out);
return out.toByteArray();
}
}
}

View File

@@ -0,0 +1,400 @@
package dev.lions.unionflow.server.service;
import static org.assertj.core.api.Assertions.*;
import dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria;
import dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import org.junit.jupiter.api.*;
/**
* Tests pour la recherche avancée de membres
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-19
*/
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class MembreServiceAdvancedSearchTest {
@Inject MembreService membreService;
@Inject MembreRepository membreRepository;
@Inject OrganisationRepository organisationRepository;
private Organisation testOrganisation;
private List<Membre> testMembres;
@BeforeEach
@Transactional
void setupTestData() {
// Créer et persister une organisation de test
testOrganisation =
Organisation.builder()
.nom("Organisation Test")
.typeOrganisation("ASSOCIATION")
.statut("ACTIF")
.email("test@organisation.com")
.build();
testOrganisation.setDateCreation(LocalDateTime.now());
testOrganisation.setActif(true);
organisationRepository.persist(testOrganisation);
// Créer des membres de test avec différents profils
testMembres =
List.of(
// Membre actif jeune
createMembre("UF-2025-TEST001", "Dupont", "Marie", "marie.dupont@test.com",
"+221701234567", LocalDate.of(1995, 5, 15), LocalDate.of(2023, 1, 15),
"MEMBRE,SECRETAIRE", true),
// Membre actif âgé
createMembre("UF-2025-TEST002", "Martin", "Jean", "jean.martin@test.com",
"+221701234568", LocalDate.of(1970, 8, 20), LocalDate.of(2020, 3, 10),
"MEMBRE,PRESIDENT", true),
// Membre inactif
createMembre("UF-2025-TEST003", "Diallo", "Fatou", "fatou.diallo@test.com",
"+221701234569", LocalDate.of(1985, 12, 3), LocalDate.of(2021, 6, 5),
"MEMBRE", false),
// Membre avec email spécifique
createMembre("UF-2025-TEST004", "Sow", "Amadou", "amadou.sow@unionflow.com",
"+221701234570", LocalDate.of(1988, 3, 12), LocalDate.of(2022, 9, 20),
"MEMBRE,TRESORIER", true));
// Persister tous les membres
testMembres.forEach(membre -> membreRepository.persist(membre));
}
private Membre createMembre(String numero, String nom, String prenom, String email,
String telephone, LocalDate dateNaissance, LocalDate dateAdhesion,
String roles, boolean actif) {
Membre membre = Membre.builder()
.numeroMembre(numero)
.nom(nom)
.prenom(prenom)
.email(email)
.telephone(telephone)
.dateNaissance(dateNaissance)
.dateAdhesion(dateAdhesion)
.organisation(testOrganisation)
.build();
membre.setDateCreation(LocalDateTime.now());
membre.setActif(actif);
// Note: Le champ roles est maintenant List<MembreRole> et doit être géré via la relation MembreRole
// Pour les tests, on laisse la liste vide par défaut
return membre;
}
@AfterEach
@Transactional
void cleanupTestData() {
// Nettoyer les données de test
if (testMembres != null) {
testMembres.forEach(membre -> {
if (membre.getId() != null) {
// Recharger l'entité depuis la base pour éviter l'erreur "detached entity"
membreRepository.findByIdOptional(membre.getId()).ifPresent(m -> {
// Utiliser deleteById pour éviter les problèmes avec les entités détachées
membreRepository.deleteById(m.getId());
});
}
});
}
if (testOrganisation != null && testOrganisation.getId() != null) {
// Recharger l'entité depuis la base pour éviter l'erreur "detached entity"
organisationRepository.findByIdOptional(testOrganisation.getId()).ifPresent(o -> {
// Utiliser deleteById pour éviter les problèmes avec les entités détachées
organisationRepository.deleteById(o.getId());
});
}
}
@Test
@Order(1)
@DisplayName("Doit effectuer une recherche par terme général")
void testSearchByGeneralQuery() {
// Given
MembreSearchCriteria criteria = MembreSearchCriteria.builder().query("marie").build();
// When
MembreSearchResultDTO result =
membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom"));
// Then
assertThat(result).isNotNull();
assertThat(result.getTotalElements()).isEqualTo(1);
assertThat(result.getMembres()).hasSize(1);
assertThat(result.getMembres().get(0).getPrenom()).isEqualToIgnoringCase("Marie");
assertThat(result.isFirst()).isTrue();
assertThat(result.isLast()).isTrue();
}
@Test
@Order(2)
@DisplayName("Doit filtrer par statut actif")
void testSearchByActiveStatus() {
// Given
MembreSearchCriteria criteria = MembreSearchCriteria.builder().statut("ACTIF").build();
// When
MembreSearchResultDTO result =
membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom"));
// Then
assertThat(result).isNotNull();
assertThat(result.getTotalElements()).isEqualTo(3); // 3 membres actifs
assertThat(result.getMembres()).hasSize(3);
assertThat(result.getMembres()).allMatch(membre -> "ACTIF".equals(membre.getStatut()));
}
@Test
@Order(3)
@DisplayName("Doit filtrer par tranche d'âge")
void testSearchByAgeRange() {
// Given
MembreSearchCriteria criteria = MembreSearchCriteria.builder().ageMin(25).ageMax(35).build();
// When
MembreSearchResultDTO result =
membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom"));
// Then
assertThat(result).isNotNull();
assertThat(result.getTotalElements()).isGreaterThan(0);
// Vérifier que tous les membres sont dans la tranche d'âge
result
.getMembres()
.forEach(
membre -> {
if (membre.getDateNaissance() != null) {
int age = LocalDate.now().getYear() - membre.getDateNaissance().getYear();
assertThat(age).isBetween(25, 35);
}
});
}
@Test
@Order(4)
@DisplayName("Doit filtrer par période d'adhésion")
void testSearchByAdhesionPeriod() {
// Given
MembreSearchCriteria criteria =
MembreSearchCriteria.builder()
.dateAdhesionMin(LocalDate.of(2022, 1, 1))
.dateAdhesionMax(LocalDate.of(2023, 12, 31))
.build();
// When
MembreSearchResultDTO result =
membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("dateAdhesion"));
// Then
assertThat(result).isNotNull();
assertThat(result.getTotalElements()).isGreaterThan(0);
// Vérifier que toutes les dates d'adhésion sont dans la période
result
.getMembres()
.forEach(
membre -> {
if (membre.getDateAdhesion() != null) {
assertThat(membre.getDateAdhesion())
.isAfterOrEqualTo(LocalDate.of(2022, 1, 1))
.isBeforeOrEqualTo(LocalDate.of(2023, 12, 31));
}
});
}
@Test
@Order(5)
@DisplayName("Doit rechercher par email avec domaine spécifique")
void testSearchByEmailDomain() {
// Given
MembreSearchCriteria criteria = MembreSearchCriteria.builder().email("@unionflow.com").build();
// When
MembreSearchResultDTO result =
membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom"));
// Then
assertThat(result).isNotNull();
assertThat(result.getTotalElements()).isEqualTo(1);
assertThat(result.getMembres()).hasSize(1);
assertThat(result.getMembres().get(0).getEmail()).contains("@unionflow.com");
}
@Test
@Order(6)
@DisplayName("Doit filtrer par rôles")
void testSearchByRoles() {
// Given
MembreSearchCriteria criteria =
MembreSearchCriteria.builder().roles(List.of("PRESIDENT", "SECRETAIRE")).build();
// When
MembreSearchResultDTO result =
membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom"));
// Then
assertThat(result).isNotNull();
assertThat(result.getTotalElements()).isGreaterThan(0);
// Vérifier que tous les membres ont au moins un des rôles recherchés
result
.getMembres()
.forEach(
membre -> {
assertThat(membre.getRole())
.satisfiesAnyOf(
role -> assertThat(role).contains("PRESIDENT"),
role -> assertThat(role).contains("SECRETAIRE"));
});
}
@Test
@Order(7)
@DisplayName("Doit gérer la pagination correctement")
void testPagination() {
// Given
MembreSearchCriteria criteria =
MembreSearchCriteria.builder()
.includeInactifs(true) // Inclure tous les membres
.build();
// When - Première page
MembreSearchResultDTO firstPage =
membreService.searchMembresAdvanced(criteria, Page.of(0, 2), Sort.by("nom"));
// Then
assertThat(firstPage).isNotNull();
assertThat(firstPage.getCurrentPage()).isEqualTo(0);
assertThat(firstPage.getPageSize()).isEqualTo(2);
assertThat(firstPage.getMembres()).hasSizeLessThanOrEqualTo(2);
assertThat(firstPage.isFirst()).isTrue();
if (firstPage.getTotalElements() > 2) {
assertThat(firstPage.isLast()).isFalse();
assertThat(firstPage.isHasNext()).isTrue();
}
}
@Test
@Order(8)
@DisplayName("Doit calculer les statistiques correctement")
void testStatisticsCalculation() {
// Given
MembreSearchCriteria criteria =
MembreSearchCriteria.builder()
.includeInactifs(true) // Inclure tous les membres
.build();
// When
MembreSearchResultDTO result =
membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom"));
// Then
assertThat(result).isNotNull();
assertThat(result.getStatistics()).isNotNull();
MembreSearchResultDTO.SearchStatistics stats = result.getStatistics();
assertThat(stats.getMembresActifs()).isEqualTo(3);
assertThat(stats.getMembresInactifs()).isEqualTo(1);
assertThat(stats.getAgeMoyen()).isGreaterThan(0);
assertThat(stats.getAgeMin()).isGreaterThan(0);
assertThat(stats.getAgeMax()).isGreaterThan(stats.getAgeMin());
assertThat(stats.getAncienneteMoyenne()).isGreaterThanOrEqualTo(0);
}
@Test
@Order(9)
@DisplayName("Doit retourner un résultat vide pour critères impossibles")
void testEmptyResultForImpossibleCriteria() {
// Given
MembreSearchCriteria criteria =
MembreSearchCriteria.builder().query("membre_inexistant_xyz").build();
// When
MembreSearchResultDTO result =
membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom"));
// Then
assertThat(result).isNotNull();
assertThat(result.getTotalElements()).isEqualTo(0);
assertThat(result.getMembres()).isEmpty();
assertThat(result.isEmpty()).isTrue();
assertThat(result.getTotalPages()).isEqualTo(0);
}
@Test
@Order(10)
@DisplayName("Doit valider la cohérence des critères")
void testCriteriaValidation() {
// Given - Critères incohérents
MembreSearchCriteria invalidCriteria =
MembreSearchCriteria.builder()
.ageMin(50)
.ageMax(30) // Âge max < âge min
.build();
// When & Then
assertThat(invalidCriteria.isValid()).isFalse();
}
@Test
@Order(11)
@DisplayName("Doit avoir des performances acceptables (< 500ms)")
void testSearchPerformance() {
// Given
MembreSearchCriteria criteria = MembreSearchCriteria.builder().includeInactifs(true).build();
// When & Then - Mesurer le temps d'exécution
long startTime = System.currentTimeMillis();
MembreSearchResultDTO result =
membreService.searchMembresAdvanced(criteria, Page.of(0, 20), Sort.by("nom"));
long executionTime = System.currentTimeMillis() - startTime;
// Vérifications
assertThat(result).isNotNull();
assertThat(executionTime).isLessThan(500L); // Moins de 500ms
// Log pour monitoring
System.out.printf(
"Recherche avancée exécutée en %d ms pour %d résultats%n",
executionTime, result.getTotalElements());
}
@Test
@Order(12)
@DisplayName("Doit gérer les critères avec caractères spéciaux")
void testSearchWithSpecialCharacters() {
// Given
MembreSearchCriteria criteria =
MembreSearchCriteria.builder().query("marie-josé").nom("o'connor").build();
// When
MembreSearchResultDTO result =
membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom"));
// Then
assertThat(result).isNotNull();
// La recherche ne doit pas échouer même avec des caractères spéciaux
assertThat(result.getTotalElements()).isGreaterThanOrEqualTo(0);
}
}