Refactoring - Version stable
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user