Initial commit: unionflow-mobile-apps

Application Flutter complète (sans build artifacts).

Signed-off-by: lions dev Team
This commit is contained in:
dahoud
2026-03-15 16:30:08 +00:00
commit d094d6db9c
1790 changed files with 507435 additions and 0 deletions

View File

@@ -0,0 +1,329 @@
package dev.lions.unionflow.server.resource;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
import dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.restassured.http.ContentType;
import java.time.LocalDate;
import java.util.List;
import org.junit.jupiter.api.*;
/**
* Tests d'intégration pour l'endpoint de recherche avancée des membres
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-19
*/
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class MembreResourceAdvancedSearchTest {
private static final String ADVANCED_SEARCH_ENDPOINT = "/api/membres/search/advanced";
@Test
@Order(1)
@TestSecurity(
user = "superadmin@unionflow.com",
roles = {"SUPER_ADMIN"})
@DisplayName("POST /api/membres/search/advanced doit fonctionner avec critères valides")
void testAdvancedSearchWithValidCriteria() {
MembreSearchCriteria criteria =
MembreSearchCriteria.builder().query("marie").statut("ACTIF").ageMin(20).ageMax(50).build();
given()
.contentType(ContentType.JSON)
.body(criteria)
.queryParam("page", 0)
.queryParam("size", 20)
.queryParam("sort", "nom")
.queryParam("direction", "asc")
.when()
.post(ADVANCED_SEARCH_ENDPOINT)
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("membres", notNullValue())
.body("totalElements", greaterThanOrEqualTo(0))
.body("totalPages", greaterThanOrEqualTo(0))
.body("currentPage", equalTo(0))
.body("pageSize", equalTo(20))
.body("hasNext", notNullValue())
.body("hasPrevious", equalTo(false))
.body("isFirst", equalTo(true))
.body("executionTimeMs", greaterThan(0))
.body("statistics", notNullValue())
.body("statistics.membresActifs", greaterThanOrEqualTo(0))
.body("statistics.membresInactifs", greaterThanOrEqualTo(0))
.body("criteria.query", equalTo("marie"))
.body("criteria.statut", equalTo("ACTIF"));
}
@Test
@Order(2)
@TestSecurity(
user = "superadmin@unionflow.com",
roles = {"SUPER_ADMIN"})
@DisplayName("POST /api/membres/search/advanced doit fonctionner avec critères multiples")
void testAdvancedSearchWithMultipleCriteria() {
MembreSearchCriteria criteria =
MembreSearchCriteria.builder()
.email("@unionflow.com")
.dateAdhesionMin(LocalDate.of(2020, 1, 1))
.dateAdhesionMax(LocalDate.of(2025, 12, 31))
.roles(List.of("ADMIN", "SUPER_ADMIN"))
.includeInactifs(false)
.build();
given()
.contentType(ContentType.JSON)
.body(criteria)
.queryParam("page", 0)
.queryParam("size", 10)
.when()
.post(ADVANCED_SEARCH_ENDPOINT)
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("membres", notNullValue())
.body("totalElements", greaterThanOrEqualTo(0))
.body("criteria.email", equalTo("@unionflow.com"))
.body("criteria.roles", hasItems("ADMIN", "SUPER_ADMIN"))
.body("criteria.includeInactifs", equalTo(false));
}
@Test
@Order(3)
@TestSecurity(
user = "superadmin@unionflow.com",
roles = {"SUPER_ADMIN"})
@DisplayName("POST /api/membres/search/advanced doit gérer la pagination")
void testAdvancedSearchPagination() {
MembreSearchCriteria criteria =
MembreSearchCriteria.builder()
.includeInactifs(true) // Inclure tous les membres
.build();
given()
.contentType(ContentType.JSON)
.body(criteria)
.queryParam("page", 0)
.queryParam("size", 2) // Petite taille pour tester la pagination
.when()
.post(ADVANCED_SEARCH_ENDPOINT)
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("currentPage", equalTo(0))
.body("pageSize", equalTo(2))
.body("isFirst", equalTo(true))
.body("hasPrevious", equalTo(false));
}
@Test
@Order(4)
@TestSecurity(
user = "superadmin@unionflow.com",
roles = {"SUPER_ADMIN"})
@DisplayName("POST /api/membres/search/advanced doit gérer le tri")
void testAdvancedSearchSorting() {
MembreSearchCriteria criteria = MembreSearchCriteria.builder().statut("ACTIF").build();
given()
.contentType(ContentType.JSON)
.body(criteria)
.queryParam("sort", "nom")
.queryParam("direction", "desc")
.when()
.post(ADVANCED_SEARCH_ENDPOINT)
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("membres", notNullValue());
}
@Test
@Order(5)
@TestSecurity(
user = "superadmin@unionflow.com",
roles = {"SUPER_ADMIN"})
@DisplayName("POST /api/membres/search/advanced doit retourner 400 pour critères vides")
void testAdvancedSearchWithEmptyCriteria() {
MembreSearchCriteria emptyCriteria = MembreSearchCriteria.builder().build();
given()
.contentType(ContentType.JSON)
.body(emptyCriteria)
.when()
.post(ADVANCED_SEARCH_ENDPOINT)
.then()
.statusCode(400)
.contentType(ContentType.JSON)
.body("message", containsString("Au moins un critère de recherche doit être spécifié"));
}
@Test
@Order(6)
@TestSecurity(
user = "superadmin@unionflow.com",
roles = {"SUPER_ADMIN"})
@DisplayName("POST /api/membres/search/advanced doit retourner 400 pour critères invalides")
void testAdvancedSearchWithInvalidCriteria() {
MembreSearchCriteria invalidCriteria =
MembreSearchCriteria.builder()
.ageMin(50)
.ageMax(30) // Âge max < âge min
.build();
given()
.contentType(ContentType.JSON)
.body(invalidCriteria)
.when()
.post(ADVANCED_SEARCH_ENDPOINT)
.then()
.statusCode(400)
.contentType(ContentType.JSON)
.body("message", containsString("Critères de recherche invalides"));
}
@Test
@Order(7)
@TestSecurity(
user = "superadmin@unionflow.com",
roles = {"SUPER_ADMIN"})
@DisplayName("POST /api/membres/search/advanced doit retourner 400 pour body null")
void testAdvancedSearchWithNullBody() {
given()
.contentType(ContentType.JSON)
.when()
.post(ADVANCED_SEARCH_ENDPOINT)
.then()
.statusCode(400);
}
@Test
@Order(8)
@TestSecurity(
user = "marie.active@unionflow.com",
roles = {"MEMBRE_ACTIF"})
@DisplayName("POST /api/membres/search/advanced doit retourner 403 pour utilisateur non autorisé")
void testAdvancedSearchUnauthorized() {
MembreSearchCriteria criteria = MembreSearchCriteria.builder().query("test").build();
given()
.contentType(ContentType.JSON)
.body(criteria)
.when()
.post(ADVANCED_SEARCH_ENDPOINT)
.then()
.statusCode(403);
}
@Test
@Order(9)
@DisplayName(
"POST /api/membres/search/advanced doit retourner 401 pour utilisateur non authentifié")
void testAdvancedSearchUnauthenticated() {
MembreSearchCriteria criteria = MembreSearchCriteria.builder().query("test").build();
given()
.contentType(ContentType.JSON)
.body(criteria)
.when()
.post(ADVANCED_SEARCH_ENDPOINT)
.then()
.statusCode(401);
}
@Test
@Order(10)
@TestSecurity(
user = "admin@unionflow.com",
roles = {"ADMIN"})
@DisplayName("POST /api/membres/search/advanced doit fonctionner pour ADMIN")
void testAdvancedSearchForAdmin() {
MembreSearchCriteria criteria = MembreSearchCriteria.builder().statut("ACTIF").build();
given()
.contentType(ContentType.JSON)
.body(criteria)
.when()
.post(ADVANCED_SEARCH_ENDPOINT)
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("membres", notNullValue());
}
@Test
@Order(11)
@TestSecurity(
user = "superadmin@unionflow.com",
roles = {"SUPER_ADMIN"})
@DisplayName("POST /api/membres/search/advanced doit inclure le temps d'exécution")
void testAdvancedSearchExecutionTime() {
MembreSearchCriteria criteria = MembreSearchCriteria.builder().query("test").build();
given()
.contentType(ContentType.JSON)
.body(criteria)
.when()
.post(ADVANCED_SEARCH_ENDPOINT)
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("executionTimeMs", greaterThan(0))
.body("executionTimeMs", lessThan(5000)); // Moins de 5 secondes
}
@Test
@Order(12)
@TestSecurity(
user = "superadmin@unionflow.com",
roles = {"SUPER_ADMIN"})
@DisplayName("POST /api/membres/search/advanced doit retourner des statistiques complètes")
void testAdvancedSearchStatistics() {
MembreSearchCriteria criteria = MembreSearchCriteria.builder().includeInactifs(true).build();
given()
.contentType(ContentType.JSON)
.body(criteria)
.when()
.post(ADVANCED_SEARCH_ENDPOINT)
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("statistics", notNullValue())
.body("statistics.membresActifs", greaterThanOrEqualTo(0))
.body("statistics.membresInactifs", greaterThanOrEqualTo(0))
.body("statistics.ageMoyen", greaterThanOrEqualTo(0.0))
.body("statistics.ageMin", greaterThanOrEqualTo(0))
.body("statistics.ageMax", greaterThanOrEqualTo(0))
.body("statistics.nombreOrganisations", greaterThanOrEqualTo(0))
.body("statistics.ancienneteMoyenne", greaterThanOrEqualTo(0.0));
}
@Test
@Order(13)
@TestSecurity(
user = "superadmin@unionflow.com",
roles = {"SUPER_ADMIN"})
@DisplayName("POST /api/membres/search/advanced doit gérer les caractères spéciaux")
void testAdvancedSearchWithSpecialCharacters() {
MembreSearchCriteria criteria =
MembreSearchCriteria.builder().query("marie-josé").nom("o'connor").build();
given()
.contentType(ContentType.JSON)
.body(criteria)
.when()
.post(ADVANCED_SEARCH_ENDPOINT)
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("membres", notNullValue());
}
}

View File

@@ -0,0 +1,392 @@
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) {
membreRepository.delete(membre);
}
});
}
if (testOrganisation != null && testOrganisation.getId() != null) {
organisationRepository.delete(testOrganisation);
}
}
@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);
}
}