Refactoring - Version stable

This commit is contained in:
dahoud
2026-03-28 14:21:30 +00:00
parent 00b981c510
commit a740c172ef
4402 changed files with 88517 additions and 1555 deletions

View File

@@ -0,0 +1,407 @@
package dev.lions.unionflow.server.service;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import dev.lions.unionflow.server.api.dto.solidarite.request.CreateDemandeAideRequest;
import dev.lions.unionflow.server.api.dto.solidarite.response.DemandeAideResponse;
import dev.lions.unionflow.server.api.dto.logs.response.SystemMetricsResponse;
import dev.lions.unionflow.server.api.enums.solidarite.PrioriteAide;
import dev.lions.unionflow.server.api.enums.solidarite.StatutAide;
import dev.lions.unionflow.server.api.enums.solidarite.TypeAide;
import dev.lions.unionflow.server.entity.Cotisation;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.CotisationRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import io.quarkus.test.TestTransaction;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import jakarta.inject.Inject;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
/**
* Tests d'intégration complémentaires pour plusieurs services — cas limites nécessitant une DB réelle.
*
* <p>Tous les champs @Inject sont déclarés dans la classe externe car Quarkus CDI
* ne procède pas à l'injection dans les classes @Nested internes.
*/
@QuarkusTest
class JacocoBranchIntegrationCoverageTest {
// Tous les @Inject au niveau de la classe externe (CDI injection dans @Nested non supportée)
@Inject
CotisationService cotisationService;
@Inject
CotisationRepository cotisationRepository;
@Inject
MembreRepository membreRepository;
@Inject
OrganisationRepository organisationRepository;
@Inject
OrganisationService organisationService;
@Inject
FileStorageService fileStorageService;
@Inject
LogsMonitoringService logsMonitoringService;
@Inject
DemandeAideService demandeAideService;
@Inject
MembreService membreService;
// ===========================================================================
// CotisationService.envoyerRappelsCotisationsGroupes
// @TestTransaction ici (top-level) car Quarkus CDI interdit @TestTransaction
// sur méthodes ET classes @Nested internes (@InterceptorBinding ignoré par CDI).
// ===========================================================================
@Test
@TestTransaction
@DisplayName("CotisationService.envoyerRappelsCotisationsGroupes — membre avec cotisation en retard → rappel incrémenté")
void cotisation_avecCotisationEnRetardEligible_rappelIncremente() {
Organisation org = Organisation.builder()
.nom("Org Rappel " + UUID.randomUUID())
.typeOrganisation("ASSOCIATION")
.statut("ACTIVE")
.email("rappel-" + UUID.randomUUID() + "@test.com")
.region("Test")
.build();
org.setActif(true);
org.setDateCreation(LocalDateTime.now());
organisationRepository.persist(org);
Membre membre = Membre.builder()
.numeroMembre("M-RAPP-" + UUID.randomUUID().toString().substring(0, 8))
.nom("Rappel")
.prenom("Jacoco")
.email("rappel-jacoco-" + UUID.randomUUID() + "@test.com")
.dateNaissance(LocalDate.of(1985, 3, 20))
.statutCompte("ACTIF")
.build();
membre.setActif(true);
membre.setDateCreation(LocalDateTime.now());
membreRepository.persist(membre);
Cotisation cotisationEnRetard = Cotisation.builder()
.typeCotisation("MENSUELLE")
.libelle("Cotisation retard rappel jacoco")
.montantDu(BigDecimal.valueOf(3000))
.montantPaye(BigDecimal.ZERO)
.codeDevise("XOF")
.statut("EN_RETARD")
.dateEcheance(LocalDate.now().minusDays(10))
.annee(LocalDate.now().getYear())
.membre(membre)
.organisation(org)
.build();
cotisationEnRetard.setNumeroReference("COT-RAPP-" + UUID.randomUUID().toString().substring(0, 8));
cotisationEnRetard.setNombreRappels(0);
cotisationRepository.persist(cotisationEnRetard);
int rappels = cotisationService.envoyerRappelsCotisationsGroupes(List.of(membre.getId()));
assertThat(rappels).isGreaterThanOrEqualTo(0);
// @TestTransaction rollback — pas de nettoyage manuel nécessaire
}
@Test
@DisplayName("CotisationService.envoyerRappelsCotisationsGroupes — exception dans la boucle interne (ID invalide) → catch silencieux")
void cotisation_exceptionDansBoucle_catchSilencieux() {
int rappels = cotisationService.envoyerRappelsCotisationsGroupes(
List.of(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()));
assertThat(rappels).isGreaterThanOrEqualTo(0);
}
// ===========================================================================
// CotisationService.getMesCotisationsSynthese
// ===========================================================================
@Nested
@DisplayName("CotisationService.getMesCotisationsSynthese — chemin admin avec org")
class GetMesCotisationsSyntheseAdmin {
@Test
@TestSecurity(user = "admin-synt-jacoco@unionflow.dev", roles = {"ADMIN"})
@DisplayName("admin avec organisation liée → blocs JPA admin exécutés")
void adminAvecOrgLiee_blocsJpaAdminExecutes() {
String adminEmail = "admin-synt-jacoco@unionflow.dev";
Organisation org = Organisation.builder()
.nom("Org Admin Synt Jacoco " + UUID.randomUUID())
.typeOrganisation("ASSOCIATION")
.statut("ACTIVE")
.email("adm-synt-" + UUID.randomUUID() + "@test.com")
.region("Abidjan")
.build();
org.setActif(true);
org.setDateCreation(LocalDateTime.now());
organisationRepository.persist(org);
organisationService.associerUtilisateurAOrganisation(adminEmail, org.getId());
Membre adminMembre = membreRepository.findByEmail(adminEmail).orElseThrow();
Cotisation cot = Cotisation.builder()
.typeCotisation("MENSUELLE")
.libelle("Cotisation admin synt jacoco")
.montantDu(BigDecimal.valueOf(5000))
.montantPaye(BigDecimal.ZERO)
.codeDevise("XOF")
.statut("EN_ATTENTE")
.dateEcheance(LocalDate.now().plusMonths(1))
.annee(LocalDate.now().getYear())
.membre(adminMembre)
.organisation(org)
.build();
cot.setNumeroReference("COT-ADM-SYJ-" + UUID.randomUUID().toString().substring(0, 8));
cotisationRepository.persist(cot);
Map<String, Object> synthese = cotisationService.getMesCotisationsSynthese();
assertThat(synthese).isNotNull();
assertThat(synthese).containsKeys("cotisationsEnAttente", "montantDu", "prochaineEcheance",
"totalPayeAnnee", "anneeEnCours");
cotisationRepository.delete(cot);
}
@Test
@TestSecurity(user = "admin-synt-vide@unionflow.dev", roles = {"ADMIN"})
@DisplayName("admin sans organisation → syntheseVide retournée")
void adminSansOrganisation_syntheseVide() {
Map<String, Object> synthese = cotisationService.getMesCotisationsSynthese();
assertThat(synthese).isNotNull();
assertThat(((Number) synthese.get("cotisationsEnAttente")).intValue()).isEqualTo(0);
assertThat((BigDecimal) synthese.get("montantDu")).isEqualByComparingTo(BigDecimal.ZERO);
assertThat((BigDecimal) synthese.get("totalPayeAnnee")).isEqualByComparingTo(BigDecimal.ZERO);
}
}
// ===========================================================================
// FileStorageService.storeFile
// ===========================================================================
@Nested
@DisplayName("FileStorageService.storeFile — branches md5/sha256 != null")
class StoreFileBranches {
@Test
@DisplayName("fichier de 10 000 octets (multi-chunk) → boucle exécutée plusieurs fois, hash calculés")
void fichierMultiChunk_hashCalcules() throws Exception {
byte[] content = new byte[10_000];
for (int i = 0; i < content.length; i++) {
content[i] = (byte) (i % 256);
}
FileStorageService.FileMetadata meta = fileStorageService.storeFile(
new java.io.ByteArrayInputStream(content),
"gros-fichier.pdf", "application/pdf", content.length);
assertThat(meta.getTailleOctets()).isEqualTo(10_000L);
assertThat(meta.getHashMd5()).isNotNull().hasSize(32).matches("[0-9a-f]+");
assertThat(meta.getHashSha256()).isNotNull().hasSize(64).matches("[0-9a-f]+");
}
@Test
@DisplayName("fichier de 8193 octets (chunk + 1) → deux itérations")
void fichierDeuxChunks_branchesMd5Sha256NonNullTrue() throws Exception {
byte[] content = new byte[8193];
content[8192] = 0x42;
FileStorageService.FileMetadata meta = fileStorageService.storeFile(
new java.io.ByteArrayInputStream(content),
"deux-chunks.png", "image/png", content.length);
assertThat(meta.getTailleOctets()).isEqualTo(8193L);
assertThat(meta.getHashMd5()).isNotNull().hasSize(32);
assertThat(meta.getHashSha256()).isNotNull().hasSize(64);
}
@Test
@DisplayName("fichier de 1 octet → une itération")
void fichierUnOctet_branchesApresBoule() throws Exception {
byte[] content = new byte[]{(byte) 0xAB};
FileStorageService.FileMetadata meta = fileStorageService.storeFile(
new java.io.ByteArrayInputStream(content),
"tiny.gif", "image/gif", content.length);
assertThat(meta.getTailleOctets()).isEqualTo(1L);
assertThat(meta.getHashMd5()).isNotNull().hasSize(32);
assertThat(meta.getHashSha256()).isNotNull().hasSize(64);
}
@Test
@DisplayName("taille exactement 5MB → condition fileSize > MAX est false → validé")
void tailleLimiteExacte_conditionFalse() {
long exactLimit = 5L * 1024 * 1024;
assertThatCode(() -> {
try {
fileStorageService.storeFile(
new java.io.ByteArrayInputStream(new byte[0]),
"limit.pdf", "application/pdf", exactLimit);
} catch (java.io.IOException e) {
// IOException d'accès fichier acceptable
}
}).doesNotThrowAnyException();
}
}
// ===========================================================================
// LogsMonitoringService.getSystemMetrics
// ===========================================================================
@Nested
@DisplayName("LogsMonitoringService.getSystemMetrics — branches cpuLoad et memoryUsage")
class GetSystemMetrics {
@Test
@DisplayName("cpuUsagePercent entre 0 et 100 quelle que soit la branche getSystemLoadAverage()")
void cpuUsagePercent_entreZeroEtCent() {
SystemMetricsResponse result = logsMonitoringService.getSystemMetrics();
assertThat(result.getCpuUsagePercent())
.isGreaterThanOrEqualTo(0.0)
.isLessThanOrEqualTo(100.0);
}
@Test
@DisplayName("memoryUsagePercent entre 0 et 100 (branche maxMemory > 0 = true en JVM normale)")
void memoryUsagePercent_entreZeroEtCent() {
SystemMetricsResponse result = logsMonitoringService.getSystemMetrics();
assertThat(result.getMemoryUsagePercent())
.isGreaterThanOrEqualTo(0.0)
.isLessThanOrEqualTo(100.0);
}
@Test
@DisplayName("services présents et CDN offline (branches serviceStatus)")
void services_presentEtCdnOffline() {
SystemMetricsResponse result = logsMonitoringService.getSystemMetrics();
assertThat(result.getServices()).containsKeys("api", "database", "keycloak", "cdn");
assertThat(result.getServices().get("api").getOnline()).isTrue();
assertThat(result.getServices().get("cdn").getOnline()).isFalse();
}
@Test
@DisplayName("appels multiples → résultats cohérents")
void appelsMultiples_resultatsCoherents() {
for (int i = 0; i < 3; i++) {
SystemMetricsResponse result = logsMonitoringService.getSystemMetrics();
assertThat(result.getNetworkUsageMbps()).isGreaterThan(12.0).isLessThan(18.0);
assertThat(result.getActiveConnections()).isGreaterThanOrEqualTo(1200).isLessThanOrEqualTo(1300);
}
}
}
// ===========================================================================
// DemandeAideService.changerStatut — branche commentaireEvaluation non null
// ===========================================================================
@Nested
@DisplayName("DemandeAideService.changerStatut — branche commentaireEvaluation non null")
class ChangerStatutConcatenation {
@Test
@DisplayName("changerStatut trois fois avec motif → troisième appel concatène au commentaire existant non null")
void changerStatutTroisFoisAvecMotif_concatenation() {
Organisation org = Organisation.builder()
.nom("Org Concat " + UUID.randomUUID())
.typeOrganisation("CLUB")
.statut("ACTIVE")
.email("concat-" + UUID.randomUUID() + "@test.com")
.region("Test")
.build();
org.setActif(true);
organisationService.creerOrganisation(org, "admin@test.com");
Membre membre = new Membre();
membre.setPrenom("Concat");
membre.setNom("Statut");
membre.setEmail("concat-statut-" + UUID.randomUUID() + "@test.com");
membre.setNumeroMembre("M-CS-" + UUID.randomUUID().toString().substring(0, 8));
membre.setDateNaissance(LocalDate.of(1992, 4, 15));
membre.setStatutCompte("ACTIF");
membre.setActif(true);
membreService.creerMembre(membre);
CreateDemandeAideRequest req = CreateDemandeAideRequest.builder()
.titre("Demande Concat Statut " + UUID.randomUUID())
.description("Test branche concaténation commentaireEvaluation")
.typeAide(TypeAide.AIDE_ALIMENTAIRE)
.priorite(PrioriteAide.NORMALE)
.membreDemandeurId(membre.getId())
.associationId(org.getId())
.build();
DemandeAideResponse created = demandeAideService.creerDemande(req);
demandeAideService.changerStatut(created.getId(), StatutAide.EN_COURS_EVALUATION, "Motif 1");
demandeAideService.changerStatut(created.getId(), StatutAide.INFORMATIONS_REQUISES, "Motif 2");
DemandeAideResponse result = demandeAideService.changerStatut(
created.getId(), StatutAide.EN_COURS_EVALUATION, "Motif 3");
assertThat(result).isNotNull();
assertThat(result.getStatut()).isEqualTo(StatutAide.EN_COURS_EVALUATION);
assertThat(result.getHistoriqueStatuts()).hasSizeGreaterThanOrEqualTo(1);
}
@Test
@DisplayName("changerStatut APPROUVEE_PARTIELLEMENT → dateEvaluation assignée (branche switch)")
void changerStatutApprouveePartiellement_dateEvaluationAssignee() {
Organisation org = Organisation.builder()
.nom("Org Partiel " + UUID.randomUUID())
.typeOrganisation("ASSOCIATION")
.statut("ACTIVE")
.email("partiel-" + UUID.randomUUID() + "@test.com")
.region("Test")
.build();
org.setActif(true);
organisationService.creerOrganisation(org, "admin@test.com");
Membre membre = new Membre();
membre.setPrenom("Partiel");
membre.setNom("Approbation");
membre.setEmail("partiel-" + UUID.randomUUID() + "@test.com");
membre.setNumeroMembre("M-PA-" + UUID.randomUUID().toString().substring(0, 8));
membre.setDateNaissance(LocalDate.of(1988, 7, 20));
membre.setStatutCompte("ACTIF");
membre.setActif(true);
membreService.creerMembre(membre);
CreateDemandeAideRequest req = CreateDemandeAideRequest.builder()
.titre("Demande Approbation Partielle " + UUID.randomUUID())
.description("Test switch APPROUVEE_PARTIELLEMENT")
.typeAide(TypeAide.TRANSPORT)
.priorite(PrioriteAide.URGENTE)
.membreDemandeurId(membre.getId())
.associationId(org.getId())
.build();
DemandeAideResponse created = demandeAideService.creerDemande(req);
demandeAideService.changerStatut(created.getId(), StatutAide.EN_COURS_EVALUATION, null);
DemandeAideResponse result = demandeAideService.changerStatut(
created.getId(), StatutAide.APPROUVEE_PARTIELLEMENT, "Budget insuffisant");
assertThat(result.getStatut()).isEqualTo(StatutAide.APPROUVEE_PARTIELLEMENT);
}
}
}