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,466 @@
package dev.lions.unionflow.server.service;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import dev.lions.unionflow.server.api.dto.analytics.KPITrendResponse;
import dev.lions.unionflow.server.api.dto.comptabilite.response.EcritureComptableResponse;
import dev.lions.unionflow.server.api.dto.comptabilite.response.LigneEcritureResponse;
import dev.lions.unionflow.server.api.dto.mutuelle.epargne.TransactionEpargneRequest;
import dev.lions.unionflow.server.api.dto.paiement.response.PaiementResponse;
import dev.lions.unionflow.server.api.dto.solidarite.request.CreateDemandeAideRequest;
import dev.lions.unionflow.server.api.enums.solidarite.TypeAide;
import dev.lions.unionflow.server.api.enums.analytics.PeriodeAnalyse;
import dev.lions.unionflow.server.api.enums.analytics.TypeMetrique;
import dev.lions.unionflow.server.api.enums.mutuelle.epargne.StatutCompteEpargne;
import dev.lions.unionflow.server.api.enums.mutuelle.epargne.TypeTransactionEpargne;
import dev.lions.unionflow.server.entity.Configuration;
import dev.lions.unionflow.server.entity.EcritureComptable;
import dev.lions.unionflow.server.entity.LigneEcriture;
import dev.lions.unionflow.server.entity.Paiement;
import dev.lions.unionflow.server.entity.mutuelle.epargne.CompteEpargne;
import dev.lions.unionflow.server.entity.mutuelle.epargne.TransactionEpargne;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.mapper.mutuelle.epargne.TransactionEpargneMapper;
import dev.lions.unionflow.server.repository.ConfigurationRepository;
import dev.lions.unionflow.server.repository.ParametresLcbFtRepository;
import dev.lions.unionflow.server.repository.PaiementRepository;
import dev.lions.unionflow.server.repository.TypeReferenceRepository;
import dev.lions.unionflow.server.repository.mutuelle.epargne.CompteEpargneRepository;
import dev.lions.unionflow.server.repository.mutuelle.epargne.TransactionEpargneRepository;
import dev.lions.unionflow.server.service.mutuelle.epargne.TransactionEpargneService;
import io.quarkus.test.InjectMock;
import io.quarkus.test.TestTransaction;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.ws.rs.NotFoundException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* Tests ciblés pour couvrir les branches manquantes restantes dans plusieurs services.
*/
@QuarkusTest
class FinalBranchCoverageTest {
// =========================================================================
// ConfigurationService.obtenirConfiguration L45 — config présente mais actif=false
// =========================================================================
@Inject
ConfigurationService configurationService;
@InjectMock
ConfigurationRepository configurationRepository;
@Test
@DisplayName("ConfigurationService.obtenirConfiguration — config trouvée mais inactif → NotFoundException")
void obtenirConfiguration_configInactive_throwsNotFound() {
Configuration config = new Configuration();
config.setCle("app.feature");
config.setActif(false);
when(configurationRepository.findByCle("app.feature"))
.thenReturn(Optional.of(config));
assertThatThrownBy(() -> configurationService.obtenirConfiguration("app.feature"))
.isInstanceOf(NotFoundException.class);
}
@Test
@DisplayName("ConfigurationService.obtenirConfiguration — config absente → NotFoundException")
void obtenirConfiguration_configAbsente_throwsNotFound() {
when(configurationRepository.findByCle("unknown.key"))
.thenReturn(Optional.empty());
assertThatThrownBy(() -> configurationService.obtenirConfiguration("unknown.key"))
.isInstanceOf(NotFoundException.class);
}
@Test
@DisplayName("ConfigurationService.obtenirConfiguration — config trouvée et active → retourne DTO")
void obtenirConfiguration_configActive_returnsDto() {
Configuration config = new Configuration();
config.setId(UUID.randomUUID());
config.setCle("app.version");
config.setValeur("1.0.0");
config.setActif(true);
when(configurationRepository.findByCle("app.version"))
.thenReturn(Optional.of(config));
var response = configurationService.obtenirConfiguration("app.version");
assertThat(response).isNotNull();
assertThat(response.getCle()).isEqualTo("app.version");
}
// =========================================================================
// DemandeAideService.creerDemande L72 — membreDemandeurId non-null mais membre introuvable
// DemandeAideService.creerDemande L79 — associationId non-null mais org introuvable
//
// Stratégie : vrais repositories + UUIDs aléatoires inexistants en base.
// BaseRepository.findById(UUID) appelle entityManager.find() directement et
// échappe au mock Mockito via Quarkus Panache — on utilise les vrais repos.
// =========================================================================
@Inject
EntityManager entityManager;
@Inject
DemandeAideService demandeAideService;
@Test
@TestTransaction
@DisplayName("DemandeAideService.creerDemande — membreDemandeurId non-null mais membre introuvable → exception (L72 true branch)")
void creerDemande_membreDemandeurNonNull_membreIntrouvable_throwsException() {
UUID membreId = UUID.randomUUID();
CreateDemandeAideRequest request = CreateDemandeAideRequest.builder()
.typeAide(TypeAide.AIDE_FINANCIERE_URGENTE)
.titre("Demande urgente")
.description("Description")
.membreDemandeurId(membreId)
.associationId(UUID.randomUUID())
.build();
assertThatThrownBy(() -> demandeAideService.creerDemande(request))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Membre demandeur non trouvé");
}
@Test
@TestTransaction
@DisplayName("DemandeAideService.creerDemande — associationId non-null mais org introuvable → exception (L79 true branch)")
void creerDemande_associationIdNonNull_orgIntrouvable_throwsException() {
Membre membre = Membre.builder()
.numeroMembre("UF-L79-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase())
.prenom("Test").nom("L79")
.email("l79." + UUID.randomUUID() + "@test.com")
.dateNaissance(LocalDate.of(1990, 1, 1))
.build();
membre.setDateCreation(LocalDateTime.now());
membre.setActif(true);
membre.setStatutCompte("ACTIF");
entityManager.persist(membre);
entityManager.flush();
UUID orgId = UUID.randomUUID();
CreateDemandeAideRequest request = CreateDemandeAideRequest.builder()
.typeAide(TypeAide.DON_MATERIEL)
.titre("Demande org")
.description("Description")
.membreDemandeurId(membre.getId())
.associationId(orgId)
.build();
assertThatThrownBy(() -> demandeAideService.creerDemande(request))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Organisation non trouvée");
}
// =========================================================================
// PaiementService.enrichirLibelles L608 — methodePaiement non-null
// =========================================================================
@Inject
PaiementService paiementService;
@InjectMock
PaiementRepository paiementRepository;
@InjectMock
TypeReferenceRepository typeReferenceRepository;
@Test
@DisplayName("PaiementService.enrichirLibelles — methodePaiement non-null → libellé résolu (branche L605 true)")
void enrichirLibelles_methodePaiementNonNull_libelleResolu() {
UUID paiementId = UUID.randomUUID();
Paiement paiement = new Paiement();
paiement.setId(paiementId);
paiement.setNumeroReference("PAY-605-TRUE");
paiement.setMontant(BigDecimal.TEN);
paiement.setCodeDevise("XOF");
paiement.setMethodePaiement("WAVE");
paiement.setStatutPaiement(null);
paiement.setActif(true);
when(paiementRepository.findPaiementById(paiementId)).thenReturn(Optional.of(paiement));
when(typeReferenceRepository.findByDomaineAndCode("METHODE_PAIEMENT", "WAVE"))
.thenReturn(Optional.empty());
PaiementResponse response = paiementService.trouverParId(paiementId);
assertThat(response).isNotNull();
assertThat(response.getMethodePaiementLibelle()).isEqualTo("WAVE");
}
// =========================================================================
// TransactionEpargneService.validerLcbFtSiSeuilAtteint L229 — montant null → early return
// L128 — montant < seuil → audit non appelé
// =========================================================================
@Inject
TransactionEpargneService transactionEpargneService;
@InjectMock
TransactionEpargneRepository transactionEpargneRepository;
@InjectMock
CompteEpargneRepository compteEpargneRepository;
@InjectMock
TransactionEpargneMapper transactionEpargneMapper;
@InjectMock
ParametresLcbFtRepository parametresLcbFtRepository;
@InjectMock
AuditService auditService;
@InjectMock
AlerteLcbFtService alerteLcbFtService;
@BeforeEach
void setupTransactionMocks() {
when(parametresLcbFtRepository.getSeuilJustification(any(), any()))
.thenReturn(Optional.of(new BigDecimal("500000")));
}
@Test
@DisplayName("TransactionEpargneService — montant null → validerLcbFtSiSeuilAtteint early return (L229 true branch)")
void executerTransaction_montantNull_validerLcbFtEarlyReturn() {
UUID compteId = UUID.randomUUID();
CompteEpargne compte = new CompteEpargne();
compte.setId(compteId);
compte.setStatut(StatutCompteEpargne.ACTIF);
compte.setSoldeActuel(new BigDecimal("1000"));
compte.setSoldeBloque(BigDecimal.ZERO);
TransactionEpargneRequest request = TransactionEpargneRequest.builder()
.compteId(compteId.toString())
.typeTransaction(TypeTransactionEpargne.DEPOT)
.montant(null)
.build();
TransactionEpargne entity = new TransactionEpargne();
when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));
when(transactionEpargneMapper.toEntity(request)).thenReturn(entity);
when(transactionEpargneMapper.toDto(entity)).thenReturn(null);
assertThatThrownBy(() -> transactionEpargneService.executerTransaction(request))
.isInstanceOf(NullPointerException.class);
}
@Test
@DisplayName("TransactionEpargneService L128 — montant < seuil → condition false → audit non appelé")
void executerTransaction_L128_montantBelowSeuil_noAudit() {
UUID compteId = UUID.randomUUID();
CompteEpargne compte = new CompteEpargne();
compte.setId(compteId);
compte.setStatut(StatutCompteEpargne.ACTIF);
compte.setSoldeActuel(new BigDecimal("2000000"));
compte.setSoldeBloque(BigDecimal.ZERO);
TransactionEpargne entity = new TransactionEpargne();
TransactionEpargneRequest request = TransactionEpargneRequest.builder()
.compteId(compteId.toString())
.typeTransaction(TypeTransactionEpargne.DEPOT)
.montant(new BigDecimal("100"))
.build();
when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));
when(transactionEpargneMapper.toEntity(request)).thenReturn(entity);
when(transactionEpargneMapper.toDto(entity)).thenReturn(null);
transactionEpargneService.executerTransaction(request);
org.mockito.Mockito.verify(auditService,
org.mockito.Mockito.never()).logLcbFtSeuilAtteint(any(), any(), any(), any(), any(), any());
}
// =========================================================================
// TrendAnalysisService.calculerTendanceLineaire L223 — denominateurR == 0
// =========================================================================
@Inject
TrendAnalysisService trendAnalysisService;
@InjectMock
KPICalculatorService kpiCalculatorService;
@Test
@DisplayName("TrendAnalysisService — toutes les valeurs égales → denominateurR=0, coefficientCorrelation=0 (branche false L223)")
void calculerTendance_toutesValeursEgales_denominateurRZero() {
UUID organisationId = UUID.randomUUID();
when(kpiCalculatorService.calculerTousLesKPI(any(), any(), any()))
.thenReturn(Map.of(TypeMetrique.NOMBRE_MEMBRES_ACTIFS, new BigDecimal("100")));
KPITrendResponse response = trendAnalysisService.calculerTendance(
TypeMetrique.NOMBRE_MEMBRES_ACTIFS,
PeriodeAnalyse.CETTE_SEMAINE,
organisationId);
assertThat(response).isNotNull();
assertThat(response.getCoefficientCorrelation()).isNotNull();
}
// =========================================================================
// ComptabiliteService — branches privées via réflexion
// L360: ecriture.getLignes() == null (false branche)
// L414: dto.lignes() == null (false branche)
// L439: ligne.getEcriture() == null (false branche)
// =========================================================================
@Inject
ComptabiliteService comptabiliteService;
@Test
@DisplayName("ComptabiliteService.convertToResponse(EcritureComptable) — lignes null → pas de setLignes (L360 false)")
void convertToResponse_ecriture_lignesNull_skipsLignes() throws Exception {
ComptabiliteService actualService =
(ComptabiliteService) ((io.quarkus.arc.ClientProxy) comptabiliteService).arc_contextualInstance();
Method method = ComptabiliteService.class.getDeclaredMethod(
"convertToResponse",
EcritureComptable.class);
method.setAccessible(true);
EcritureComptable ecriture = new EcritureComptable();
ecriture.setLibelle("Test sans lignes");
// Force lignes=null via réflexion pour couvrir la branche false (null) à L360
java.lang.reflect.Field lignesField = EcritureComptable.class.getDeclaredField("lignes");
lignesField.setAccessible(true);
lignesField.set(ecriture, null);
Object result = method.invoke(actualService, ecriture);
assertThat(result).isNotNull();
var response = (EcritureComptableResponse) result;
assertThat(response.getLignes()).isNullOrEmpty();
}
@Test
@DisplayName("EcritureComptable.onCreate() — lignes null → branche lignes!=null false (L163)")
void ecritureComptable_onCreate_lignesNull_skipsCalculerTotaux() throws Exception {
EcritureComptable ecriture = new EcritureComptable();
ecriture.setLibelle("Test L163");
ecriture.setDateEcriture(LocalDate.now());
// Force lignes=null pour couvrir la branche A=false de "lignes != null && !lignes.isEmpty()" à L163
java.lang.reflect.Field lignesField = EcritureComptable.class.getDeclaredField("lignes");
lignesField.setAccessible(true);
lignesField.set(ecriture, null);
// Appel direct de @PrePersist via réflexion
java.lang.reflect.Method onCreateMethod = EcritureComptable.class.getDeclaredMethod("onCreate");
onCreateMethod.setAccessible(true);
onCreateMethod.invoke(ecriture);
// La méthode doit s'exécuter sans exception
assertThat(ecriture.getLibelle()).isEqualTo("Test L163");
}
@Test
@DisplayName("TransactionEpargneService L128 — getMontant() retourne null au 4ème appel → A=false branche couverte")
void executerTransaction_L128_getMontantNullOnFinalCheck_falseBranchCovered() {
UUID compteId = UUID.randomUUID();
CompteEpargne compte = new CompteEpargne();
compte.setId(compteId);
compte.setStatut(StatutCompteEpargne.ACTIF);
compte.setSoldeActuel(new BigDecimal("1000"));
compte.setSoldeBloque(BigDecimal.ZERO);
TransactionEpargne entity = new TransactionEpargne();
// Séquence getMontant() :
// Appel 1 (L229) : 10 → non-null → ne pas retourner early
// Appel 2 (L232) : 10 → 10 < 500000 → pas d'erreur LCB-FT
// Appel 3 (L80) : 10 → montant=10 pour les calculs de solde
// Appel 4 (L128) : null → null != null → false → branche A=false couverte!
TransactionEpargneRequest realRequest = TransactionEpargneRequest.builder()
.compteId(compteId.toString())
.typeTransaction(TypeTransactionEpargne.DEPOT)
.montant(new BigDecimal("10"))
.build();
TransactionEpargneRequest spyRequest = org.mockito.Mockito.spy(realRequest);
org.mockito.Mockito.doReturn(new BigDecimal("10"),
new BigDecimal("10"),
new BigDecimal("10"),
(BigDecimal) null)
.when(spyRequest).getMontant();
when(compteEpargneRepository.findByIdOptional(compteId)).thenReturn(Optional.of(compte));
when(transactionEpargneMapper.toEntity(any())).thenReturn(entity);
when(transactionEpargneMapper.toDto(entity)).thenReturn(null);
transactionEpargneService.executerTransaction(spyRequest);
org.mockito.Mockito.verify(auditService, org.mockito.Mockito.never())
.logLcbFtSeuilAtteint(any(), any(), any(), any(), any(), any());
}
@Test
@DisplayName("ComptabiliteService.convertToResponse(LigneEcriture) — ecriture null → ecritureId null (L439 false)")
void convertToResponse_ligne_ecritureNull_ecritureIdNull() throws Exception {
ComptabiliteService actualService =
(ComptabiliteService) ((io.quarkus.arc.ClientProxy) comptabiliteService).arc_contextualInstance();
Method method = ComptabiliteService.class.getDeclaredMethod(
"convertToResponse",
LigneEcriture.class);
method.setAccessible(true);
LigneEcriture ligne = new LigneEcriture();
ligne.setLibelle("Ligne sans écriture");
ligne.setMontantDebit(BigDecimal.TEN);
ligne.setMontantCredit(BigDecimal.ZERO);
Object result = method.invoke(actualService, ligne);
assertThat(result).isNotNull();
var response = (LigneEcritureResponse) result;
assertThat(response.getEcritureId()).isNull();
}
@Test
@DisplayName("ComptabiliteService.convertToEntity(CreateEcritureComptableRequest) — lignes null → pas d'ajout lignes (L414 false)")
void convertToEntity_ecriture_lignesNull_skipsLignesAdd() throws Exception {
ComptabiliteService actualService =
(ComptabiliteService) ((io.quarkus.arc.ClientProxy) comptabiliteService).arc_contextualInstance();
Method method = ComptabiliteService.class.getDeclaredMethod(
"convertToEntity",
dev.lions.unionflow.server.api.dto.comptabilite.request.CreateEcritureComptableRequest.class);
method.setAccessible(true);
var dto = dev.lions.unionflow.server.api.dto.comptabilite.request.CreateEcritureComptableRequest.builder()
.numeroPiece("PIECE-NULL-LIGNES")
.dateEcriture(java.time.LocalDate.now())
.libelle("Écriture sans lignes")
.montantDebit(new BigDecimal("500"))
.montantCredit(new BigDecimal("500"))
.lignes(null)
.build();
Object result = method.invoke(actualService, dto);
assertThat(result).isNotNull();
EcritureComptable ecriture = (EcritureComptable) result;
assertThat(ecriture.getLignes()).isEmpty();
}
}