Alignement design systeme OK

This commit is contained in:
DahoudG
2025-09-20 03:56:11 +00:00
parent a1214bc116
commit 96a17eadbd
34 changed files with 11720 additions and 766 deletions

View File

@@ -1,10 +1,13 @@
package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.api.dto.membre.MembreDTO;
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.service.MembreService;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
@@ -12,8 +15,15 @@ import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.ExampleObject;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.logging.Logger;
@@ -186,8 +196,9 @@ public class MembreResource {
@GET
@Path("/recherche-avancee")
@Operation(summary = "Recherche avancée de membres avec filtres multiples")
@Operation(summary = "Recherche avancée de membres avec filtres multiples (DEPRECATED)")
@APIResponse(responseCode = "200", description = "Résultats de la recherche avancée")
@Deprecated
public Response rechercheAvancee(
@Parameter(description = "Terme de recherche") @QueryParam("q") String recherche,
@Parameter(description = "Statut actif (true/false)") @QueryParam("actif") Boolean actif,
@@ -198,7 +209,7 @@ public class MembreResource {
@Parameter(description = "Champ de tri") @QueryParam("sort") @DefaultValue("nom") String sortField,
@Parameter(description = "Direction du tri (asc/desc)") @QueryParam("direction") @DefaultValue("asc") String sortDirection) {
LOG.infof("Recherche avancée de membres - recherche: %s, actif: %s", recherche, actif);
LOG.infof("Recherche avancée de membres (DEPRECATED) - recherche: %s, actif: %s", recherche, actif);
try {
Sort sort = "desc".equalsIgnoreCase(sortDirection) ?
@@ -222,4 +233,178 @@ public class MembreResource {
.build();
}
}
/**
* Nouvelle recherche avancée avec critères complets et résultats enrichis
* Réservée aux super administrateurs pour des recherches sophistiquées
*/
@POST
@Path("/search/advanced")
@RolesAllowed({"SUPER_ADMIN", "ADMIN"})
@Operation(
summary = "Recherche avancée de membres avec critères multiples",
description = """
Recherche sophistiquée de membres avec de nombreux critères de filtrage :
- Recherche textuelle dans nom, prénom, email
- Filtres par organisation, rôles, statut
- Filtres par âge, région, profession
- Filtres par dates d'adhésion
- Résultats paginés avec statistiques
Réservée aux super administrateurs et administrateurs.
"""
)
@APIResponses({
@APIResponse(
responseCode = "200",
description = "Recherche effectuée avec succès",
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = MembreSearchResultDTO.class),
examples = @ExampleObject(
name = "Exemple de résultats",
value = """
{
"membres": [...],
"totalElements": 247,
"totalPages": 13,
"currentPage": 0,
"pageSize": 20,
"hasNext": true,
"hasPrevious": false,
"executionTimeMs": 45,
"statistics": {
"membresActifs": 230,
"membresInactifs": 17,
"ageMoyen": 34.5,
"nombreOrganisations": 12
}
}
"""
)
)
),
@APIResponse(
responseCode = "400",
description = "Critères de recherche invalides",
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
examples = @ExampleObject(
value = """
{
"message": "Critères de recherche invalides",
"details": "La date minimum ne peut pas être postérieure à la date maximum"
}
"""
)
)
),
@APIResponse(
responseCode = "403",
description = "Accès non autorisé - Rôle SUPER_ADMIN ou ADMIN requis"
),
@APIResponse(
responseCode = "500",
description = "Erreur interne du serveur"
)
})
@SecurityRequirement(name = "keycloak")
public Response searchMembresAdvanced(
@RequestBody(
description = "Critères de recherche avancée",
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = MembreSearchCriteria.class),
examples = @ExampleObject(
name = "Exemple de critères",
value = """
{
"query": "marie",
"statut": "ACTIF",
"ageMin": 25,
"ageMax": 45,
"region": "Dakar",
"roles": ["PRESIDENT", "SECRETAIRE"],
"dateAdhesionMin": "2020-01-01",
"includeInactifs": false
}
"""
)
)
)
@Valid MembreSearchCriteria criteria,
@Parameter(description = "Numéro de page (0-based)", example = "0")
@QueryParam("page") @DefaultValue("0") int page,
@Parameter(description = "Taille de la page", example = "20")
@QueryParam("size") @DefaultValue("20") int size,
@Parameter(description = "Champ de tri", example = "nom")
@QueryParam("sort") @DefaultValue("nom") String sortField,
@Parameter(description = "Direction du tri (asc/desc)", example = "asc")
@QueryParam("direction") @DefaultValue("asc") String sortDirection) {
long startTime = System.currentTimeMillis();
LOG.infof("Recherche avancée de membres - critères: %s, page: %d, size: %d",
criteria.getDescription(), page, size);
try {
// Validation des critères
if (criteria == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("message", "Les critères de recherche sont requis"))
.build();
}
// Nettoyage et validation des critères
criteria.sanitize();
if (!criteria.hasAnyCriteria()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("message", "Au moins un critère de recherche doit être spécifié"))
.build();
}
if (!criteria.isValid()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of(
"message", "Critères de recherche invalides",
"details", "Vérifiez la cohérence des dates et des âges"
))
.build();
}
// Construction du tri
Sort sort = "desc".equalsIgnoreCase(sortDirection) ?
Sort.by(sortField).descending() : Sort.by(sortField).ascending();
// Exécution de la recherche
MembreSearchResultDTO result = membreService.searchMembresAdvanced(
criteria, Page.of(page, size), sort);
// Calcul du temps d'exécution
long executionTime = System.currentTimeMillis() - startTime;
result.setExecutionTimeMs(executionTime);
LOG.infof("Recherche avancée terminée - %d résultats trouvés en %d ms",
result.getTotalElements(), executionTime);
return Response.ok(result).build();
} catch (IllegalArgumentException e) {
LOG.warnf("Erreur de validation dans la recherche avancée: %s", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("message", "Paramètres de recherche invalides", "details", e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche avancée de membres");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("message", "Erreur interne lors de la recherche", "error", e.getMessage()))
.build();
}
}
}

View File

@@ -1,6 +1,8 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.membre.MembreDTO;
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.repository.MembreRepository;
import io.quarkus.panache.common.Page;
@@ -12,6 +14,9 @@ import org.jboss.logging.Logger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -285,14 +290,240 @@ public class MembreService {
}
/**
* Recherche avancée de membres avec filtres multiples
* Recherche avancée de membres avec filtres multiples (DEPRECATED)
*/
public List<Membre> rechercheAvancee(String recherche, Boolean actif,
LocalDate dateAdhesionMin, LocalDate dateAdhesionMax,
Page page, Sort sort) {
LOG.infof("Recherche avancée - recherche: %s, actif: %s, dateMin: %s, dateMax: %s",
LOG.infof("Recherche avancée (DEPRECATED) - recherche: %s, actif: %s, dateMin: %s, dateMax: %s",
recherche, actif, dateAdhesionMin, dateAdhesionMax);
return membreRepository.rechercheAvancee(recherche, actif, dateAdhesionMin, dateAdhesionMax, page, sort);
}
/**
* Nouvelle recherche avancée de membres avec critères complets
* Retourne des résultats paginés avec statistiques
*
* @param criteria Critères de recherche
* @param page Pagination
* @param sort Tri
* @return Résultats de recherche avec métadonnées
*/
public MembreSearchResultDTO searchMembresAdvanced(MembreSearchCriteria criteria, Page page, Sort sort) {
LOG.infof("Recherche avancée de membres - critères: %s", criteria.getDescription());
try {
// Construction de la requête dynamique
StringBuilder queryBuilder = new StringBuilder("SELECT m FROM Membre m WHERE 1=1");
Map<String, Object> parameters = new HashMap<>();
// Ajout des critères de recherche
addSearchCriteria(queryBuilder, parameters, criteria);
// Requête pour compter le total
String countQuery = queryBuilder.toString().replace("SELECT m FROM Membre m", "SELECT COUNT(m) FROM Membre m");
// Exécution de la requête de comptage
long totalElements = Membre.find(countQuery, parameters).count();
if (totalElements == 0) {
return MembreSearchResultDTO.empty(criteria);
}
// Ajout du tri et pagination
String finalQuery = queryBuilder.toString();
if (sort != null) {
finalQuery += " ORDER BY " + buildOrderByClause(sort);
}
// Exécution de la requête principale
List<Membre> membres = Membre.find(finalQuery, parameters)
.page(page)
.list();
// Conversion en DTOs
List<MembreDTO> membresDTO = convertToDTOList(membres);
// Calcul des statistiques
MembreSearchResultDTO.SearchStatistics statistics = calculateSearchStatistics(membres);
// Construction du résultat
MembreSearchResultDTO result = MembreSearchResultDTO.builder()
.membres(membresDTO)
.totalElements(totalElements)
.totalPages((int) Math.ceil((double) totalElements / page.size))
.currentPage(page.index)
.pageSize(page.size)
.criteria(criteria)
.statistics(statistics)
.build();
// Calcul des indicateurs de pagination
result.calculatePaginationFlags();
return result;
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche avancée de membres");
throw new RuntimeException("Erreur lors de la recherche avancée", e);
}
}
/**
* Ajoute les critères de recherche à la requête
*/
private void addSearchCriteria(StringBuilder queryBuilder, Map<String, Object> parameters, MembreSearchCriteria criteria) {
// Recherche générale dans nom, prénom, email
if (criteria.getQuery() != null) {
queryBuilder.append(" AND (LOWER(m.nom) LIKE LOWER(:query) OR LOWER(m.prenom) LIKE LOWER(:query) OR LOWER(m.email) LIKE LOWER(:query))");
parameters.put("query", "%" + criteria.getQuery() + "%");
}
// Recherche par nom
if (criteria.getNom() != null) {
queryBuilder.append(" AND LOWER(m.nom) LIKE LOWER(:nom)");
parameters.put("nom", "%" + criteria.getNom() + "%");
}
// Recherche par prénom
if (criteria.getPrenom() != null) {
queryBuilder.append(" AND LOWER(m.prenom) LIKE LOWER(:prenom)");
parameters.put("prenom", "%" + criteria.getPrenom() + "%");
}
// Recherche par email
if (criteria.getEmail() != null) {
queryBuilder.append(" AND LOWER(m.email) LIKE LOWER(:email)");
parameters.put("email", "%" + criteria.getEmail() + "%");
}
// Recherche par téléphone
if (criteria.getTelephone() != null) {
queryBuilder.append(" AND m.telephone LIKE :telephone");
parameters.put("telephone", "%" + criteria.getTelephone() + "%");
}
// Filtre par statut
if (criteria.getStatut() != null) {
boolean isActif = "ACTIF".equals(criteria.getStatut());
queryBuilder.append(" AND m.actif = :actif");
parameters.put("actif", isActif);
} else if (!Boolean.TRUE.equals(criteria.getIncludeInactifs())) {
// Par défaut, exclure les inactifs
queryBuilder.append(" AND m.actif = true");
}
// Filtre par dates d'adhésion
if (criteria.getDateAdhesionMin() != null) {
queryBuilder.append(" AND m.dateAdhesion >= :dateAdhesionMin");
parameters.put("dateAdhesionMin", criteria.getDateAdhesionMin());
}
if (criteria.getDateAdhesionMax() != null) {
queryBuilder.append(" AND m.dateAdhesion <= :dateAdhesionMax");
parameters.put("dateAdhesionMax", criteria.getDateAdhesionMax());
}
// Filtre par âge (calculé à partir de la date de naissance)
if (criteria.getAgeMin() != null) {
LocalDate maxBirthDate = LocalDate.now().minusYears(criteria.getAgeMin());
queryBuilder.append(" AND m.dateNaissance <= :maxBirthDateForMinAge");
parameters.put("maxBirthDateForMinAge", maxBirthDate);
}
if (criteria.getAgeMax() != null) {
LocalDate minBirthDate = LocalDate.now().minusYears(criteria.getAgeMax() + 1).plusDays(1);
queryBuilder.append(" AND m.dateNaissance >= :minBirthDateForMaxAge");
parameters.put("minBirthDateForMaxAge", minBirthDate);
}
// Filtre par organisations (si implémenté dans l'entité)
if (criteria.getOrganisationIds() != null && !criteria.getOrganisationIds().isEmpty()) {
queryBuilder.append(" AND m.organisation.id IN :organisationIds");
parameters.put("organisationIds", criteria.getOrganisationIds());
}
// Filtre par rôles (recherche dans le champ roles)
if (criteria.getRoles() != null && !criteria.getRoles().isEmpty()) {
StringBuilder roleCondition = new StringBuilder(" AND (");
for (int i = 0; i < criteria.getRoles().size(); i++) {
if (i > 0) roleCondition.append(" OR ");
roleCondition.append("m.roles LIKE :role").append(i);
parameters.put("role" + i, "%" + criteria.getRoles().get(i) + "%");
}
roleCondition.append(")");
queryBuilder.append(roleCondition);
}
}
/**
* Construit la clause ORDER BY à partir du Sort
*/
private String buildOrderByClause(Sort sort) {
if (sort == null || sort.getColumns().isEmpty()) {
return "m.nom ASC";
}
return sort.getColumns().stream()
.map(column -> "m." + column.getName() + " " + column.getDirection().name())
.collect(Collectors.joining(", "));
}
/**
* Calcule les statistiques sur les résultats de recherche
*/
private MembreSearchResultDTO.SearchStatistics calculateSearchStatistics(List<Membre> membres) {
if (membres.isEmpty()) {
return MembreSearchResultDTO.SearchStatistics.builder()
.membresActifs(0)
.membresInactifs(0)
.ageMoyen(0.0)
.ageMin(0)
.ageMax(0)
.nombreOrganisations(0)
.nombreRegions(0)
.ancienneteMoyenne(0.0)
.build();
}
long membresActifs = membres.stream().mapToLong(m -> Boolean.TRUE.equals(m.getActif()) ? 1 : 0).sum();
long membresInactifs = membres.size() - membresActifs;
// Calcul des âges
List<Integer> ages = membres.stream()
.filter(m -> m.getDateNaissance() != null)
.map(m -> Period.between(m.getDateNaissance(), LocalDate.now()).getYears())
.collect(Collectors.toList());
double ageMoyen = ages.stream().mapToInt(Integer::intValue).average().orElse(0.0);
int ageMin = ages.stream().mapToInt(Integer::intValue).min().orElse(0);
int ageMax = ages.stream().mapToInt(Integer::intValue).max().orElse(0);
// Calcul de l'ancienneté moyenne
double ancienneteMoyenne = membres.stream()
.filter(m -> m.getDateAdhesion() != null)
.mapToDouble(m -> Period.between(m.getDateAdhesion(), LocalDate.now()).getYears())
.average()
.orElse(0.0);
// Nombre d'organisations (si relation disponible)
long nombreOrganisations = membres.stream()
.filter(m -> m.getOrganisation() != null)
.map(m -> m.getOrganisation().id)
.distinct()
.count();
return MembreSearchResultDTO.SearchStatistics.builder()
.membresActifs(membresActifs)
.membresInactifs(membresInactifs)
.ageMoyen(ageMoyen)
.ageMin(ageMin)
.ageMax(ageMax)
.nombreOrganisations(nombreOrganisations)
.nombreRegions(0) // À implémenter si champ région disponible
.ancienneteMoyenne(ancienneteMoyenne)
.build();
}
}

View File

@@ -32,7 +32,7 @@ quarkus.flyway.baseline-on-migrate=true
quarkus.flyway.baseline-version=1.0.0
# Configuration Keycloak OIDC
quarkus.oidc.auth-server-url=http://192.168.1.145:8180/realms/unionflow
quarkus.oidc.auth-server-url=http://192.168.1.11:8180/realms/unionflow
quarkus.oidc.client-id=unionflow-server
quarkus.oidc.credentials.secret=unionflow-secret-2025
quarkus.oidc.tls.verification=none
@@ -85,7 +85,7 @@ quarkus.log.category."io.quarkus".level=INFO
# Configuration Keycloak pour développement (temporairement désactivé)
%dev.quarkus.oidc.tenant-enabled=false
%dev.quarkus.oidc.auth-server-url=http://192.168.1.145:8180/realms/unionflow
%dev.quarkus.oidc.auth-server-url=http://192.168.1.11:8180/realms/unionflow
%dev.quarkus.oidc.client-id=unionflow-server
%dev.quarkus.oidc.credentials.secret=unionflow-secret-2025
%dev.quarkus.oidc.tls.verification=none
@@ -114,7 +114,7 @@ quarkus.log.category."io.quarkus".level=INFO
%prod.quarkus.log.category.root.level=WARN
# Configuration Keycloak pour production
%prod.quarkus.oidc.auth-server-url=${KEYCLOAK_SERVER_URL:http://192.168.1.145:8180/realms/unionflow}
%prod.quarkus.oidc.auth-server-url=${KEYCLOAK_SERVER_URL:http://192.168.1.11:8180/realms/unionflow}
%prod.quarkus.oidc.client-id=${KEYCLOAK_CLIENT_ID:unionflow-server}
%prod.quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET}
%prod.quarkus.oidc.tls.verification=required

View File

@@ -0,0 +1,320 @@
package dev.lions.unionflow.server.resource;
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 org.junit.jupiter.api.*;
import java.time.LocalDate;
import java.util.List;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
/**
* 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,409 @@
package dev.lions.unionflow.server.service;
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 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 org.junit.jupiter.api.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
/**
* 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;
private static Organisation testOrganisation;
private static List<Membre> testMembres;
@BeforeAll
@Transactional
static void setupTestData() {
// Créer une organisation de test
testOrganisation = Organisation.builder()
.nom("Organisation Test")
.typeOrganisation("ASSOCIATION")
.statut("ACTIF")
.actif(true)
.dateCreation(LocalDateTime.now())
.build();
testOrganisation.persist();
// Créer des membres de test avec différents profils
testMembres = List.of(
// Membre actif jeune
Membre.builder()
.numeroMembre("UF-2025-TEST001")
.nom("Dupont")
.prenom("Marie")
.email("marie.dupont@test.com")
.telephone("+221701234567")
.dateNaissance(LocalDate.of(1995, 5, 15))
.dateAdhesion(LocalDate.of(2023, 1, 15))
.roles("MEMBRE,SECRETAIRE")
.actif(true)
.organisation(testOrganisation)
.dateCreation(LocalDateTime.now())
.build(),
// Membre actif âgé
Membre.builder()
.numeroMembre("UF-2025-TEST002")
.nom("Martin")
.prenom("Jean")
.email("jean.martin@test.com")
.telephone("+221701234568")
.dateNaissance(LocalDate.of(1970, 8, 20))
.dateAdhesion(LocalDate.of(2020, 3, 10))
.roles("MEMBRE,PRESIDENT")
.actif(true)
.organisation(testOrganisation)
.dateCreation(LocalDateTime.now())
.build(),
// Membre inactif
Membre.builder()
.numeroMembre("UF-2025-TEST003")
.nom("Diallo")
.prenom("Fatou")
.email("fatou.diallo@test.com")
.telephone("+221701234569")
.dateNaissance(LocalDate.of(1985, 12, 3))
.dateAdhesion(LocalDate.of(2021, 6, 5))
.roles("MEMBRE")
.actif(false)
.organisation(testOrganisation)
.dateCreation(LocalDateTime.now())
.build(),
// Membre avec email spécifique
Membre.builder()
.numeroMembre("UF-2025-TEST004")
.nom("Sow")
.prenom("Amadou")
.email("amadou.sow@unionflow.com")
.telephone("+221701234570")
.dateNaissance(LocalDate.of(1988, 3, 12))
.dateAdhesion(LocalDate.of(2022, 9, 20))
.roles("MEMBRE,TRESORIER")
.actif(true)
.organisation(testOrganisation)
.dateCreation(LocalDateTime.now())
.build()
);
// Persister tous les membres
testMembres.forEach(membre -> membre.persist());
}
@AfterAll
@Transactional
static void cleanupTestData() {
// Nettoyer les données de test
if (testMembres != null) {
testMembres.forEach(membre -> {
if (membre.isPersistent()) {
membre.delete();
}
});
}
if (testOrganisation != null && testOrganisation.isPersistent()) {
testOrganisation.delete();
}
}
@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);
}
}