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(); } }