Alignement design systeme OK
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user