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.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import dev.lions.unionflow.server.api.dto.membre.request.CreateMembreRequest; import dev.lions.unionflow.server.api.dto.membre.request.UpdateMembreRequest; import dev.lions.unionflow.server.api.dto.membre.response.MembreResponse; import dev.lions.unionflow.server.api.dto.membre.response.MembreSummaryResponse; import dev.lions.unionflow.server.entity.Membre; import dev.lions.unionflow.server.entity.MembreOrganisation; import dev.lions.unionflow.server.entity.MembreRole; import dev.lions.unionflow.server.entity.Organisation; import dev.lions.unionflow.server.entity.Role; import dev.lions.unionflow.server.repository.MembreRepository; import dev.lions.unionflow.server.repository.MembreRoleRepository; import dev.lions.unionflow.server.repository.TypeReferenceRepository; import io.quarkus.panache.common.Page; import io.quarkus.panache.common.Sort; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.test.InjectMock; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.mockito.InjectSpy; import io.quarkus.test.TestTransaction; import jakarta.inject.Inject; import jakarta.persistence.EntityManager; import java.security.Principal; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @QuarkusTest class MembreServiceTest { @Inject MembreService membreService; @InjectSpy MembreRepository membreRepository; @Inject EntityManager em; @InjectMock MembreRoleRepository membreRoleRepository; @InjectMock TypeReferenceRepository typeReferenceRepository; @InjectMock MembreImportExportService membreImportExportService; @InjectMock OrganisationService organisationService; @InjectMock SecurityIdentity securityIdentity; // ─── Helpers ────────────────────────────────────────────────────────────── private Membre membreFixture(String email) { Membre m = new Membre(); m.setId(UUID.randomUUID()); m.setEmail(email); m.setNom("Doe"); m.setPrenom("John"); m.setDateNaissance(LocalDate.of(1990, 1, 1)); m.setNumeroMembre("UF2024-ABCD1234"); m.setActif(true); m.setStatutCompte("ACTIF"); m.setVersion(0L); m.setMembresOrganisations(new ArrayList<>()); m.setAdresses(new ArrayList<>()); return m; } /** By default, securityIdentity behaves as anonymous (no ADMIN_ORGANISATION). */ @BeforeEach void setupDefaultSecurity() { Principal principal = () -> "anonymous"; when(securityIdentity.getPrincipal()).thenReturn(principal); when(securityIdentity.getRoles()).thenReturn(Set.of()); } // ========================================================================= // creerMembre // ========================================================================= @Nested @DisplayName("creerMembre") class CreerMembreTests { @Test @DisplayName("Happy path: numéro généré automatiquement, statut EN_ATTENTE_VALIDATION") void creerMembre_generatesNumeroAndSetsEnAttenteValidation() { Membre membre = new Membre(); membre.setEmail("test@unionflow.dev"); membre.setNom("Doe"); membre.setPrenom("John"); membre.setDateNaissance(LocalDate.of(1990, 1, 1)); when(membreRepository.findByEmail("test@unionflow.dev")).thenReturn(Optional.empty()); when(membreRepository.findByNumeroMembre(any())).thenReturn(Optional.empty()); doNothing().when(membreRepository).persist(any(Membre.class)); Membre created = membreService.creerMembre(membre); assertThat(created.getNumeroMembre()).startsWith("UF"); assertThat(created.getStatutCompte()).isEqualTo("EN_ATTENTE_VALIDATION"); assertThat(created.getActif()).isFalse(); verify(membreRepository).persist(membre); } @Test @DisplayName("Numéro déjà fourni: conservé tel quel") void creerMembre_withExistingNumero_keepsIt() { Membre membre = new Membre(); membre.setEmail("test2@unionflow.dev"); membre.setNom("Smith"); membre.setPrenom("Jane"); membre.setNumeroMembre("UF2024-CUSTOM"); membre.setDateNaissance(LocalDate.of(1985, 5, 15)); when(membreRepository.findByEmail(anyString())).thenReturn(Optional.empty()); when(membreRepository.findByNumeroMembre("UF2024-CUSTOM")).thenReturn(Optional.empty()); doNothing().when(membreRepository).persist(any(Membre.class)); Membre created = membreService.creerMembre(membre); assertThat(created.getNumeroMembre()).isEqualTo("UF2024-CUSTOM"); } @Test @DisplayName("Date de naissance null: valeur par défaut 18 ans") void creerMembre_nullDateNaissance_setsDefault() { Membre membre = new Membre(); membre.setEmail("nodate@unionflow.dev"); membre.setNom("NoDate"); membre.setPrenom("User"); // dateNaissance = null intentionally when(membreRepository.findByEmail(anyString())).thenReturn(Optional.empty()); when(membreRepository.findByNumeroMembre(any())).thenReturn(Optional.empty()); doNothing().when(membreRepository).persist(any(Membre.class)); Membre created = membreService.creerMembre(membre); assertThat(created.getDateNaissance()).isNotNull(); assertThat(created.getDateNaissance()).isEqualTo(LocalDate.now().minusYears(18)); } @Test @DisplayName("Email déjà existant: lève IllegalArgumentException") void creerMembre_duplicateEmail_throws() { Membre membre = new Membre(); membre.setEmail("dup@unionflow.dev"); membre.setNom("Dup"); membre.setPrenom("User"); when(membreRepository.findByEmail("dup@unionflow.dev")) .thenReturn(Optional.of(membreFixture("dup@unionflow.dev"))); assertThatThrownBy(() -> membreService.creerMembre(membre)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("email existe déjà"); verify(membreRepository, never()).persist(any(Membre.class)); } @Test @DisplayName("Numéro de membre déjà existant: lève IllegalArgumentException") void creerMembre_duplicateNumero_throws() { Membre membre = new Membre(); membre.setEmail("unique@unionflow.dev"); membre.setNom("Unique"); membre.setPrenom("User"); membre.setNumeroMembre("UF2024-EXIST"); when(membreRepository.findByEmail("unique@unionflow.dev")).thenReturn(Optional.empty()); when(membreRepository.findByNumeroMembre("UF2024-EXIST")) .thenReturn(Optional.of(membreFixture("other@unionflow.dev"))); assertThatThrownBy(() -> membreService.creerMembre(membre)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("numéro existe déjà"); verify(membreRepository, never()).persist(any(Membre.class)); } } // ========================================================================= // activerMembre // ========================================================================= @Nested @DisplayName("activerMembre") class ActiverMembreTests { @Test @DisplayName("Happy path: membre EN_ATTENTE_VALIDATION → ACTIF + actif=true") void activerMembre_pendingMember_setsActif() { UUID id = UUID.randomUUID(); Membre membre = membreFixture("pending@unionflow.dev"); membre.setId(id); membre.setStatutCompte("EN_ATTENTE_VALIDATION"); membre.setActif(false); when(membreRepository.findByIdOptional(id)).thenReturn(Optional.of(membre)); doNothing().when(membreRepository).persist(any(Membre.class)); Membre activated = membreService.activerMembre(id); assertThat(activated.getStatutCompte()).isEqualTo("ACTIF"); assertThat(activated.getActif()).isTrue(); verify(membreRepository).persist(membre); } @Test @DisplayName("Membre introuvable: lève NotFoundException") void activerMembre_notFound_throws() { UUID id = UUID.randomUUID(); when(membreRepository.findByIdOptional(id)).thenReturn(Optional.empty()); assertThatThrownBy(() -> membreService.activerMembre(id)) .isInstanceOf(jakarta.ws.rs.NotFoundException.class) .hasMessageContaining("non trouvé"); verify(membreRepository, never()).persist(any(Membre.class)); } } // ========================================================================= // promouvoirAdminOrganisation // ========================================================================= @Nested @DisplayName("promouvoirAdminOrganisation") class PromouvoirAdminOrganisationTests { @Test @DisplayName("Happy path: membre promu → ACTIF + actif=true immédiatement") void promouvoirAdminOrganisation_setsActifImmediately() { UUID id = UUID.randomUUID(); Membre membre = new Membre(); membre.setId(id); membre.setEmail("future-admin@unionflow.dev"); membre.setNom("Admin"); membre.setPrenom("New"); membre.setStatutCompte("EN_ATTENTE_VALIDATION"); membre.setActif(false); when(membreRepository.findByIdOptional(id)).thenReturn(Optional.of(membre)); doNothing().when(membreRepository).persist(any(Membre.class)); Membre result = membreService.promouvoirAdminOrganisation(id); assertThat(result.getStatutCompte()).isEqualTo("ACTIF"); assertThat(result.getActif()).isTrue(); verify(membreRepository).persist(membre); } @Test @DisplayName("Membre introuvable: lève NotFoundException") void promouvoirAdminOrganisation_notFound_throws() { UUID id = UUID.randomUUID(); when(membreRepository.findByIdOptional(id)).thenReturn(Optional.empty()); assertThatThrownBy(() -> membreService.promouvoirAdminOrganisation(id)) .isInstanceOf(jakarta.ws.rs.NotFoundException.class) .hasMessageContaining("non trouvé"); verify(membreRepository, never()).persist(any(Membre.class)); } } // ========================================================================= // mettreAJourMembre // ========================================================================= @Nested @DisplayName("mettreAJourMembre") class MettreAJourMembreTests { @Test @DisplayName("Membre introuvable: lève IllegalArgumentException") void mettreAJourMembre_notFound_throws() { UUID id = UUID.randomUUID(); // UUID aléatoire non présent en H2 → findById retourne null → service throws Membre modifie = new Membre(); modifie.setEmail("any@unionflow.dev"); modifie.setNom("X"); modifie.setPrenom("Y"); assertThatThrownBy(() -> membreService.mettreAJourMembre(id, modifie)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("non trouvé"); } } // ========================================================================= // trouverParId / trouverParEmail // ========================================================================= @Nested @DisplayName("trouverParId / trouverParEmail") class TrouverTests { @Test @DisplayName("trouverParId: retourne empty si non trouvé") void trouverParId_notFound() { UUID id = UUID.randomUUID(); // UUID aléatoire non présent en H2 → findById retourne null → trouverParId retourne empty Optional result = membreService.trouverParId(id); assertThat(result).isEmpty(); } @Test @DisplayName("trouverParEmail: délègue au repository") void trouverParEmail_delegates() { Membre m = membreFixture("byemail@unionflow.dev"); when(membreRepository.findByEmail("byemail@unionflow.dev")).thenReturn(Optional.of(m)); Optional result = membreService.trouverParEmail("byemail@unionflow.dev"); assertThat(result).isPresent(); } } // ========================================================================= // listerMembresActifs / rechercherMembres (simple) // ========================================================================= @Nested @DisplayName("listerMembresActifs / rechercherMembres simple") class ListerTests { @Test @DisplayName("listerMembresActifs sans pagination: délègue au repository") void listerMembresActifs_delegates() { List membres = List.of(membreFixture("a@test.dev"), membreFixture("b@test.dev")); when(membreRepository.findAllActifs()).thenReturn(membres); List result = membreService.listerMembresActifs(); assertThat(result).hasSize(2); } @Test @DisplayName("listerMembresActifs avec pagination: délègue au repository") void listerMembresActifs_paged() { Page page = Page.of(0, 10); Sort sort = Sort.by("nom").ascending(); List membres = List.of(membreFixture("c@test.dev")); when(membreRepository.findAllActifs(page, sort)).thenReturn(membres); List result = membreService.listerMembresActifs(page, sort); assertThat(result).hasSize(1); } @Test @DisplayName("rechercherMembres sans pagination: délègue au repository") void rechercherMembres_simple() { List membres = List.of(membreFixture("search@test.dev")); when(membreRepository.findByNomOrPrenom("doe")).thenReturn(membres); List result = membreService.rechercherMembres("doe"); assertThat(result).hasSize(1); } @Test @DisplayName("compterMembresActifs: délègue au repository") void compterMembresActifs() { when(membreRepository.countActifs()).thenReturn(42L); long count = membreService.compterMembresActifs(); assertThat(count).isEqualTo(42L); } } // ========================================================================= // desactiverMembre // ========================================================================= @Nested @DisplayName("desactiverMembre") class DesactiverTests { @Test @DisplayName("Membre introuvable: lève IllegalArgumentException") void desactiverMembre_notFound_throws() { UUID id = UUID.randomUUID(); // UUID aléatoire non présent en H2 → findById retourne null → service throws assertThatThrownBy(() -> membreService.desactiverMembre(id)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("non trouvé"); } } // ========================================================================= // listerMembres (avec logique ADMIN_ORGANISATION) // ========================================================================= @Nested @DisplayName("listerMembres (avec logique ADMIN_ORGANISATION)") class ListerMembresSecurityTests { @Test @DisplayName("Utilisateur sans rôle ADMIN_ORGANISATION: liste tous les membres") void listerMembres_noAdminOrgRole_returnsAll() { Page page = Page.of(0, 10); Sort sort = Sort.by("nom").ascending(); List all = List.of(membreFixture("x@test.dev")); when(securityIdentity.getRoles()).thenReturn(Set.of("USER")); doReturn(all).when(membreRepository).findAll(page, sort); List result = membreService.listerMembres(page, sort); assertThat(result).hasSize(1); verify(membreRepository).findAll(page, sort); } @Test @DisplayName("ADMIN_ORGANISATION avec organisations: filtre par organisations") void listerMembres_adminOrg_filtersToOrgs() { Page page = Page.of(0, 10); Sort sort = Sort.by("nom").ascending(); UUID orgId = UUID.randomUUID(); Organisation org = new Organisation(); org.setId(orgId); org.setNom("Test Org"); Principal principal = () -> "admin@org.dev"; when(securityIdentity.getPrincipal()).thenReturn(principal); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION")); when(organisationService.listerOrganisationsPourUtilisateur("admin@org.dev")) .thenReturn(List.of(org)); List orgMembres = List.of(membreFixture("m@test.dev")); when(membreRepository.findDistinctByOrganisationIdIn(any(), eq(page), eq(sort))) .thenReturn(orgMembres); List result = membreService.listerMembres(page, sort); assertThat(result).hasSize(1); } @Test @DisplayName("ADMIN_ORGANISATION sans organisations: retourne liste vide") void listerMembres_adminOrg_noOrgs_returnsEmpty() { Page page = Page.of(0, 10); Sort sort = Sort.by("nom").ascending(); Principal principal = () -> "admin@org.dev"; when(securityIdentity.getPrincipal()).thenReturn(principal); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION")); when(organisationService.listerOrganisationsPourUtilisateur("admin@org.dev")) .thenReturn(List.of()); List result = membreService.listerMembres(page, sort); assertThat(result).isEmpty(); } @Test @DisplayName("ADMIN_ORGANISATION + ADMIN: pas de restriction (retourne tous)") void listerMembres_adminOrgAndAdmin_returnsAll() { Page page = Page.of(0, 10); Sort sort = Sort.by("nom").ascending(); List all = List.of(membreFixture("y@test.dev")); Principal principal = () -> "admin@org.dev"; when(securityIdentity.getPrincipal()).thenReturn(principal); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION", "ADMIN")); when(membreRepository.findAll(page, sort)).thenReturn(all); List result = membreService.listerMembres(page, sort); assertThat(result).hasSize(1); } @Test @DisplayName("SecurityIdentity null (principal null): retourne tous les membres") void listerMembres_nullPrincipal_returnsAll() { Page page = Page.of(0, 10); Sort sort = Sort.by("nom").ascending(); List all = List.of(membreFixture("z@test.dev")); when(securityIdentity.getPrincipal()).thenReturn(null); when(membreRepository.findAll(page, sort)).thenReturn(all); List result = membreService.listerMembres(page, sort); assertThat(result).hasSize(1); } @Test @DisplayName("Roles null: retourne tous les membres") void listerMembres_nullRoles_returnsAll() { Page page = Page.of(0, 10); Sort sort = Sort.by("nom").ascending(); List all = List.of(membreFixture("z2@test.dev")); Principal principal = () -> "user@test.dev"; when(securityIdentity.getPrincipal()).thenReturn(principal); when(securityIdentity.getRoles()).thenReturn(null); when(membreRepository.findAll(page, sort)).thenReturn(all); List result = membreService.listerMembres(page, sort); assertThat(result).hasSize(1); } } // ========================================================================= // compterMembres (avec logique ADMIN_ORGANISATION) // ========================================================================= @Nested @DisplayName("compterMembres") class CompterMembresTests { @Test @DisplayName("Utilisateur normal: compte tous les membres") void compterMembres_noAdminOrg_countsAll() { when(securityIdentity.getRoles()).thenReturn(Set.of()); doReturn(100L).when(membreRepository).count(); long count = membreService.compterMembres(); assertThat(count).isEqualTo(100L); } @Test @DisplayName("ADMIN_ORGANISATION avec organisations: compte par org") void compterMembres_adminOrg_countsFiltered() { UUID orgId = UUID.randomUUID(); Organisation org = new Organisation(); org.setId(orgId); Principal principal = () -> "admin@org.dev"; when(securityIdentity.getPrincipal()).thenReturn(principal); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION")); when(organisationService.listerOrganisationsPourUtilisateur("admin@org.dev")) .thenReturn(List.of(org)); when(membreRepository.countDistinctByOrganisationIdIn(any())).thenReturn(15L); long count = membreService.compterMembres(); assertThat(count).isEqualTo(15L); } @Test @DisplayName("ADMIN_ORGANISATION sans organisations: retourne 0") void compterMembres_adminOrg_noOrgs_returnsZero() { Principal principal = () -> "admin@org.dev"; when(securityIdentity.getPrincipal()).thenReturn(principal); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION")); when(organisationService.listerOrganisationsPourUtilisateur("admin@org.dev")) .thenReturn(List.of()); long count = membreService.compterMembres(); assertThat(count).isEqualTo(0L); } } // ========================================================================= // rechercherMembres avec pagination // ========================================================================= @Nested @DisplayName("rechercherMembres (avec pagination)") class RechercherMembresPagedTests { @Test @DisplayName("Utilisateur normal: recherche tous") void rechercherMembres_paged_noAdminOrg() { Page page = Page.of(0, 10); Sort sort = Sort.by("nom").ascending(); List membres = List.of(membreFixture("r@test.dev")); when(securityIdentity.getRoles()).thenReturn(Set.of()); when(membreRepository.findByNomOrPrenom("doe", page, sort)).thenReturn(membres); List result = membreService.rechercherMembres("doe", page, sort); assertThat(result).hasSize(1); } @Test @DisplayName("ADMIN_ORGANISATION avec organisations: filtre") void rechercherMembres_paged_adminOrg() { Page page = Page.of(0, 10); Sort sort = Sort.by("nom").ascending(); UUID orgId = UUID.randomUUID(); Organisation org = new Organisation(); org.setId(orgId); Principal principal = () -> "admin@org.dev"; when(securityIdentity.getPrincipal()).thenReturn(principal); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION")); when(organisationService.listerOrganisationsPourUtilisateur("admin@org.dev")) .thenReturn(List.of(org)); when(membreRepository.findByNomOrPrenomAndOrganisationIdIn(eq("doe"), any(), eq(page), eq(sort))) .thenReturn(List.of(membreFixture("r@test.dev"))); List result = membreService.rechercherMembres("doe", page, sort); assertThat(result).hasSize(1); } @Test @DisplayName("ADMIN_ORGANISATION sans organisations: retourne liste vide") void rechercherMembres_paged_adminOrg_noOrgs() { Page page = Page.of(0, 10); Sort sort = Sort.by("nom").ascending(); Principal principal = () -> "admin@org.dev"; when(securityIdentity.getPrincipal()).thenReturn(principal); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION")); when(organisationService.listerOrganisationsPourUtilisateur("admin@org.dev")) .thenReturn(List.of()); List result = membreService.rechercherMembres("doe", page, sort); assertThat(result).isEmpty(); } } // ========================================================================= // obtenirStatistiquesAvancees // ========================================================================= @Test @DisplayName("obtenirStatistiquesAvancees: calcule le taux d'activité correctement") void obtenirStatistiquesAvancees_calculatesCorrectly() { doReturn(100L).when(membreRepository).count(); doReturn(80L).when(membreRepository).countActifs(); doReturn(10L).when(membreRepository).countNouveauxMembres(any()); Map stats = membreService.obtenirStatistiquesAvancees(); assertThat(stats).containsKey("totalMembres"); assertThat(stats).containsKey("membresActifs"); assertThat(stats).containsKey("membresInactifs"); assertThat(stats).containsKey("tauxActivite"); assertThat(stats.get("totalMembres")).isEqualTo(100L); assertThat(stats.get("membresActifs")).isEqualTo(80L); assertThat(stats.get("membresInactifs")).isEqualTo(20L); assertThat((Double) stats.get("tauxActivite")).isEqualTo(80.0); } @Test @DisplayName("obtenirStatistiquesAvancees: taux 0 si pas de membres") void obtenirStatistiquesAvancees_noMembers_tauxZero() { doReturn(0L).when(membreRepository).count(); doReturn(0L).when(membreRepository).countActifs(); doReturn(0L).when(membreRepository).countNouveauxMembres(any()); Map stats = membreService.obtenirStatistiquesAvancees(); assertThat((Double) stats.get("tauxActivite")).isEqualTo(0.0); } // ========================================================================= // convertToResponse // ========================================================================= @Nested @DisplayName("convertToResponse") class ConvertToResponseTests { @Test @DisplayName("null retourne null") void convertToResponse_null() { assertThat(membreService.convertToResponse(null)).isNull(); } @Test @DisplayName("Membre minimal sans organisation ni rôles") void convertToResponse_minimalMembre() { Membre m = membreFixture("resp@test.dev"); m.setStatutCompte(null); m.setStatutMatrimonial(null); m.setTypeIdentite(null); m.setMembresOrganisations(new ArrayList<>()); when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of()); MembreResponse resp = membreService.convertToResponse(m); assertThat(resp).isNotNull(); assertThat(resp.getEmail()).isEqualTo("resp@test.dev"); assertThat(resp.getRoles()).isEmpty(); assertThat(resp.getOrganisationId()).isNull(); } @Test @DisplayName("Membre avec statut matrimonial et typeIdentite: résout les libellés") void convertToResponse_withReferenceData() { Membre m = membreFixture("full@test.dev"); m.setStatutMatrimonial("MARIE"); m.setTypeIdentite("CNI"); m.setStatutCompte("ACTIF"); m.setMembresOrganisations(new ArrayList<>()); m.setVersion(2L); when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of()); when(typeReferenceRepository.findLibelleByDomaineAndCode("STATUT_MATRIMONIAL", "MARIE")) .thenReturn("Marié(e)"); when(typeReferenceRepository.findLibelleByDomaineAndCode("TYPE_IDENTITE", "CNI")) .thenReturn("Carte Nationale d'Identité"); when(typeReferenceRepository.findLibelleByDomaineAndCode("STATUT_COMPTE", "ACTIF")) .thenReturn("Actif"); when(typeReferenceRepository.findSeverityByDomaineAndCode("STATUT_COMPTE", "ACTIF")) .thenReturn("success"); MembreResponse resp = membreService.convertToResponse(m); assertThat(resp.getStatutMatrimonialLibelle()).isEqualTo("Marié(e)"); assertThat(resp.getTypeIdentiteLibelle()).isEqualTo("Carte Nationale d'Identité"); assertThat(resp.getStatutCompteLibelle()).isEqualTo("Actif"); assertThat(resp.getStatutCompteSeverity()).isEqualTo("success"); assertThat(resp.getVersion()).isEqualTo(2L); } @Test @DisplayName("Membre avec rôles actifs: les codes sont inclus") void convertToResponse_withRoles() { Membre m = membreFixture("roles@test.dev"); m.setMembresOrganisations(new ArrayList<>()); Role role = new Role(); role.setCode("PRESIDENT"); MembreRole mr = new MembreRole(); mr.setRole(role); when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of(mr)); MembreResponse resp = membreService.convertToResponse(m); assertThat(resp.getRoles()).contains("PRESIDENT"); } @Test @DisplayName("Membre avec rôle dont role est null: filtré") void convertToResponse_roleNullFiltered() { Membre m = membreFixture("nullrole@test.dev"); m.setMembresOrganisations(new ArrayList<>()); MembreRole mr = new MembreRole(); mr.setRole(null); // null role when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of(mr)); MembreResponse resp = membreService.convertToResponse(m); assertThat(resp.getRoles()).isEmpty(); } @Test @DisplayName("Membre avec organisation: organisationId et nom remplis") void convertToResponse_withOrganisation() { UUID orgId = UUID.randomUUID(); Organisation org = new Organisation(); org.setId(orgId); org.setNom("Lions Club"); MembreOrganisation mo = new MembreOrganisation(); mo.setOrganisation(org); mo.setDateAdhesion(LocalDate.of(2022, 6, 1)); mo.setMembre(new Membre()); Membre m = membreFixture("org@test.dev"); m.setMembresOrganisations(new ArrayList<>(List.of(mo))); when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of()); MembreResponse resp = membreService.convertToResponse(m); assertThat(resp.getOrganisationId()).isEqualTo(orgId); assertThat(resp.getAssociationNom()).isEqualTo("Lions Club"); assertThat(resp.getDateAdhesion()).isEqualTo(LocalDate.of(2022, 6, 1)); } @Test @DisplayName("Membre avec MembreOrganisation dont organisation est null: pas d'exception") void convertToResponse_moWithNullOrg() { MembreOrganisation mo = new MembreOrganisation(); mo.setOrganisation(null); mo.setMembre(new Membre()); Membre m = membreFixture("nullorg@test.dev"); m.setMembresOrganisations(new ArrayList<>(List.of(mo))); when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of()); MembreResponse resp = membreService.convertToResponse(m); assertThat(resp.getOrganisationId()).isNull(); assertThat(resp.getAssociationNom()).isNull(); } @Test @DisplayName("Version null: retourne 0") void convertToResponse_versionNull() { Membre m = membreFixture("ver@test.dev"); m.setVersion(null); m.setMembresOrganisations(new ArrayList<>()); when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of()); MembreResponse resp = membreService.convertToResponse(m); assertThat(resp.getVersion()).isEqualTo(0L); } } // ========================================================================= // convertToSummaryResponse // ========================================================================= @Nested @DisplayName("convertToSummaryResponse") class ConvertToSummaryResponseTests { @Test @DisplayName("null retourne null") void convertToSummaryResponse_null() { assertThat(membreService.convertToSummaryResponse(null)).isNull(); } @Test @DisplayName("Membre sans rôles ni statut: rôles vides, libellés null") void convertToSummaryResponse_noRolesNoStatut() { Membre m = membreFixture("sum@test.dev"); m.setStatutCompte(null); m.setMembresOrganisations(new ArrayList<>()); when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of()); MembreSummaryResponse resp = membreService.convertToSummaryResponse(m); assertThat(resp).isNotNull(); assertThat(resp.roles()).isEmpty(); assertThat(resp.statutCompteLibelle()).isNull(); assertThat(resp.statutCompteSeverity()).isNull(); } @Test @DisplayName("Membre avec statut ACTIF: libellé et severity résolus") void convertToSummaryResponse_withStatut() { Membre m = membreFixture("statut@test.dev"); m.setStatutCompte("ACTIF"); m.setMembresOrganisations(new ArrayList<>()); when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of()); when(typeReferenceRepository.findLibelleByDomaineAndCode("STATUT_COMPTE", "ACTIF")) .thenReturn("Actif"); when(typeReferenceRepository.findSeverityByDomaineAndCode("STATUT_COMPTE", "ACTIF")) .thenReturn("success"); MembreSummaryResponse resp = membreService.convertToSummaryResponse(m); assertThat(resp.statutCompteLibelle()).isEqualTo("Actif"); assertThat(resp.statutCompteSeverity()).isEqualTo("success"); } @Test @DisplayName("Membre avec rôles actifs: codes inclus") void convertToSummaryResponse_withRoles() { Membre m = membreFixture("sumroles@test.dev"); m.setMembresOrganisations(new ArrayList<>()); Role role = new Role(); role.setCode("TRESORIER"); MembreRole mr = new MembreRole(); mr.setRole(role); when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of(mr)); MembreSummaryResponse resp = membreService.convertToSummaryResponse(m); assertThat(resp.roles()).contains("TRESORIER"); } @Test @DisplayName("Membre avec organisation: organisationId rempli") void convertToSummaryResponse_withOrganisation() { UUID orgId = UUID.randomUUID(); Organisation org = new Organisation(); org.setId(orgId); org.setNom("Assoc Test"); MembreOrganisation mo = new MembreOrganisation(); mo.setOrganisation(org); mo.setMembre(new Membre()); Membre m = membreFixture("sumorg@test.dev"); m.setMembresOrganisations(new ArrayList<>(List.of(mo))); when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of()); MembreSummaryResponse resp = membreService.convertToSummaryResponse(m); assertThat(resp.organisationId()).isEqualTo(orgId); assertThat(resp.associationNom()).isEqualTo("Assoc Test"); } @Test @DisplayName("MembreOrganisation avec organisation null: pas d'exception") void convertToSummaryResponse_moWithNullOrg() { MembreOrganisation mo = new MembreOrganisation(); mo.setOrganisation(null); mo.setMembre(new Membre()); Membre m = membreFixture("nullorgsum@test.dev"); m.setMembresOrganisations(new ArrayList<>(List.of(mo))); when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of()); MembreSummaryResponse resp = membreService.convertToSummaryResponse(m); assertThat(resp.organisationId()).isNull(); } } // ========================================================================= // convertFromCreateRequest // ========================================================================= @Nested @DisplayName("convertFromCreateRequest") class ConvertFromCreateRequestTests { @Test @DisplayName("null retourne null") void convertFromCreateRequest_null() { assertThat(membreService.convertFromCreateRequest(null)).isNull(); } @Test @DisplayName("DTO complet: tous les champs copiés") void convertFromCreateRequest_fullDto() { LocalDate dateNaissance = LocalDate.of(1990, 5, 15); CreateMembreRequest req = CreateMembreRequest.builder() .nom("Diallo") .prenom("Aminata") .email("aminata@test.dev") .telephone("0600000001") .telephoneWave("+2250700000001") .dateNaissance(dateNaissance) .profession("Ingénieure") .photoUrl("https://example.com/photo.jpg") .statutMatrimonial("CELIBATAIRE") .nationalite("Ivoirienne") .typeIdentite("CNI") .numeroIdentite("ABC123456") .organisationId(UUID.randomUUID()) .build(); Membre membre = membreService.convertFromCreateRequest(req); assertThat(membre.getNom()).isEqualTo("Diallo"); assertThat(membre.getPrenom()).isEqualTo("Aminata"); assertThat(membre.getEmail()).isEqualTo("aminata@test.dev"); assertThat(membre.getTelephone()).isEqualTo("0600000001"); assertThat(membre.getTelephoneWave()).isEqualTo("+2250700000001"); assertThat(membre.getDateNaissance()).isEqualTo(dateNaissance); assertThat(membre.getProfession()).isEqualTo("Ingénieure"); assertThat(membre.getPhotoUrl()).isEqualTo("https://example.com/photo.jpg"); assertThat(membre.getStatutMatrimonial()).isEqualTo("CELIBATAIRE"); assertThat(membre.getNationalite()).isEqualTo("Ivoirienne"); assertThat(membre.getTypeIdentite()).isEqualTo("CNI"); assertThat(membre.getNumeroIdentite()).isEqualTo("ABC123456"); } } // ========================================================================= // updateFromRequest // ========================================================================= @Nested @DisplayName("updateFromRequest") class UpdateFromRequestTests { @Test @DisplayName("null membre ou null dto: aucune mise à jour") void updateFromRequest_nullInputs_noOp() { Membre m = membreFixture("before@test.dev"); // Both null membreService.updateFromRequest(null, null); // null dto membreService.updateFromRequest(m, null); // null membre UpdateMembreRequest req = UpdateMembreRequest.builder() .nom("X") .prenom("Y") .email("z@test.dev") .dateNaissance(LocalDate.now()) .build(); membreService.updateFromRequest(null, req); // email remains unchanged on original membre assertThat(m.getEmail()).isEqualTo("before@test.dev"); } @Test @DisplayName("DTO complet avec actif non null: tous les champs mis à jour") void updateFromRequest_fullDto_allUpdated() { Membre m = membreFixture("old@test.dev"); LocalDate newDate = LocalDate.of(1995, 8, 20); UpdateMembreRequest req = UpdateMembreRequest.builder() .nom("NouveauNom") .prenom("NouveauPrenom") .email("new@test.dev") .telephone("0700000002") .telephoneWave("+2250700000002") .dateNaissance(newDate) .profession("Médecin") .photoUrl("https://example.com/new.jpg") .statutMatrimonial("MARIE") .nationalite("Sénégalaise") .typeIdentite("PASSEPORT") .numeroIdentite("PA123456") .actif(false) .build(); membreService.updateFromRequest(m, req); assertThat(m.getNom()).isEqualTo("NouveauNom"); assertThat(m.getPrenom()).isEqualTo("NouveauPrenom"); assertThat(m.getEmail()).isEqualTo("new@test.dev"); assertThat(m.getTelephone()).isEqualTo("0700000002"); assertThat(m.getDateNaissance()).isEqualTo(newDate); assertThat(m.getProfession()).isEqualTo("Médecin"); assertThat(m.getActif()).isFalse(); assertThat(m.getDateModification()).isNotNull(); } @Test @DisplayName("actif null dans le DTO: flag actif non modifié") void updateFromRequest_actifNull_notModified() { Membre m = membreFixture("keep@test.dev"); m.setActif(true); UpdateMembreRequest req = UpdateMembreRequest.builder() .nom("KeepActif") .prenom("Test") .email("keep@test.dev") .dateNaissance(LocalDate.now()) .actif(null) // null → should not change actif .build(); membreService.updateFromRequest(m, req); assertThat(m.getActif()).isTrue(); } } // ========================================================================= // convertToResponseList / convertToSummaryResponseList // ========================================================================= @Nested @DisplayName("convertToResponseList / convertToSummaryResponseList") class ConvertListTests { @Test @DisplayName("liste null: retourne liste vide") void convertToResponseList_null_returnsEmpty() { List result = membreService.convertToResponseList(null); assertThat(result).isEmpty(); } @Test @DisplayName("liste vide: retourne liste vide") void convertToResponseList_empty_returnsEmpty() { List result = membreService.convertToResponseList(new ArrayList<>()); assertThat(result).isEmpty(); } @Test @DisplayName("liste de 2 membres: retourne 2 réponses") void convertToResponseList_twoMembres() { Membre m1 = membreFixture("a@test.dev"); Membre m2 = membreFixture("b@test.dev"); when(membreRoleRepository.findActifsByMembreId(m1.getId())).thenReturn(List.of()); when(membreRoleRepository.findActifsByMembreId(m2.getId())).thenReturn(List.of()); List result = membreService.convertToResponseList(List.of(m1, m2)); assertThat(result).hasSize(2); } @Test @DisplayName("convertToSummaryResponseList null: retourne liste vide") void convertToSummaryResponseList_null() { List result = membreService.convertToSummaryResponseList(null); assertThat(result).isEmpty(); } @Test @DisplayName("convertToSummaryResponseList de 1 membre") void convertToSummaryResponseList_oneMembre() { Membre m = membreFixture("s@test.dev"); m.setMembresOrganisations(new ArrayList<>()); when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of()); List result = membreService.convertToSummaryResponseList(List.of(m)); assertThat(result).hasSize(1); } } // ========================================================================= // rechercheAvancee (deprecated) // ========================================================================= @Test @DisplayName("rechercheAvancee (deprecated): délègue au repository") void rechercheAvancee_deprecated_delegates() { Page page = Page.of(0, 10); Sort sort = Sort.by("nom").ascending(); List membres = List.of(membreFixture("adv@test.dev")); LocalDate min = LocalDate.of(2020, 1, 1); LocalDate max = LocalDate.of(2024, 12, 31); when(membreRepository.rechercheAvancee("john", true, min, max, page, sort)) .thenReturn(membres); List result = membreService.rechercheAvancee("john", true, min, max, page, sort); assertThat(result).hasSize(1); } // ========================================================================= // exporterMembresSelectionnes // ========================================================================= @Nested @DisplayName("exporterMembresSelectionnes") class ExporterMembresTests { @Test @DisplayName("liste null: lève IllegalArgumentException") void exporterMembresSelectionnes_null_throws() { assertThatThrownBy(() -> membreService.exporterMembresSelectionnes(null, "CSV")) .isInstanceOf(IllegalArgumentException.class); } @Test @DisplayName("liste vide: lève IllegalArgumentException") void exporterMembresSelectionnes_empty_throws() { assertThatThrownBy(() -> membreService.exporterMembresSelectionnes(List.of(), "CSV")) .isInstanceOf(IllegalArgumentException.class); } @Test @DisplayName("membres non trouvés: retourne CSV avec en-tête seulement") void exporterMembresSelectionnes_notFound_returnsHeaderOnlyCsv() { UUID id = UUID.randomUUID(); when(membreRepository.findByIdOptional(id)).thenReturn(Optional.empty()); byte[] result = membreService.exporterMembresSelectionnes(List.of(id), "CSV"); assertThat(result).isNotNull(); String csv = new String(result, java.nio.charset.StandardCharsets.UTF_8); assertThat(csv).contains("Numéro"); } @Test @DisplayName("membres trouvés: retourne CSV avec données") void exporterMembresSelectionnes_found_returnsCsvWithData() { UUID id = UUID.randomUUID(); Membre m = membreFixture("exp@test.dev"); m.setId(id); m.setMembresOrganisations(new ArrayList<>()); when(membreRepository.findByIdOptional(id)).thenReturn(Optional.of(m)); when(membreRoleRepository.findActifsByMembreId(id)).thenReturn(List.of()); byte[] result = membreService.exporterMembresSelectionnes(List.of(id), "CSV"); assertThat(result).isNotNull(); String csv = new String(result, java.nio.charset.StandardCharsets.UTF_8); assertThat(csv).contains("exp@test.dev"); } } // ========================================================================= // exporterVersExcel / exporterVersCSV / exporterVersPDF / genererModeleImport // ========================================================================= @Nested @DisplayName("Export/Import delegation tests") class ExportImportDelegationTests { @Test @DisplayName("exporterVersExcel: délègue et retourne résultat") void exporterVersExcel_delegates() throws Exception { byte[] data = "excel".getBytes(); when(membreImportExportService.exporterVersExcel(any(), any(), eq(true), eq(true), eq(false), any())) .thenReturn(data); byte[] result = membreService.exporterVersExcel(List.of(), List.of(), true, true, false, null); assertThat(result).isEqualTo(data); } @Test @DisplayName("exporterVersExcel: exception propagée comme RuntimeException") void exporterVersExcel_exception_wraps() throws Exception { when(membreImportExportService.exporterVersExcel(any(), any(), anyBoolean(), anyBoolean(), anyBoolean(), any())) .thenThrow(new RuntimeException("Excel error")); assertThatThrownBy(() -> membreService.exporterVersExcel(List.of(), List.of(), true, true, false, null)) .isInstanceOf(RuntimeException.class) .hasMessageContaining("Excel"); } @Test @DisplayName("exporterVersCSV: délègue et retourne résultat") void exporterVersCSV_delegates() throws Exception { byte[] data = "csv".getBytes(); when(membreImportExportService.exporterVersCSV(any(), any(), eq(true), eq(false))) .thenReturn(data); byte[] result = membreService.exporterVersCSV(List.of(), List.of(), true, false); assertThat(result).isEqualTo(data); } @Test @DisplayName("exporterVersCSV: exception propagée comme RuntimeException") void exporterVersCSV_exception_wraps() throws Exception { when(membreImportExportService.exporterVersCSV(any(), any(), anyBoolean(), anyBoolean())) .thenThrow(new RuntimeException("CSV error")); assertThatThrownBy(() -> membreService.exporterVersCSV(List.of(), List.of(), true, false)) .isInstanceOf(RuntimeException.class) .hasMessageContaining("CSV"); } @Test @DisplayName("exporterVersPDF: délègue et retourne résultat") void exporterVersPDF_delegates() throws Exception { byte[] data = "pdf".getBytes(); when(membreImportExportService.exporterVersPDF(any(), any(), eq(true), eq(false), eq(true))) .thenReturn(data); byte[] result = membreService.exporterVersPDF(List.of(), List.of(), true, false, true); assertThat(result).isEqualTo(data); } @Test @DisplayName("exporterVersPDF: exception propagée comme RuntimeException") void exporterVersPDF_exception_wraps() throws Exception { when(membreImportExportService.exporterVersPDF(any(), any(), anyBoolean(), anyBoolean(), anyBoolean())) .thenThrow(new RuntimeException("PDF error")); assertThatThrownBy(() -> membreService.exporterVersPDF(List.of(), List.of(), true, false, true)) .isInstanceOf(RuntimeException.class) .hasMessageContaining("PDF"); } @Test @DisplayName("genererModeleImport: délègue et retourne résultat") void genererModeleImport_delegates() throws Exception { byte[] data = "template".getBytes(); when(membreImportExportService.genererModeleImport()).thenReturn(data); byte[] result = membreService.genererModeleImport(); assertThat(result).isEqualTo(data); } @Test @DisplayName("genererModeleImport: exception propagée comme RuntimeException") void genererModeleImport_exception_wraps() throws Exception { when(membreImportExportService.genererModeleImport()).thenThrow(new RuntimeException("Template error")); assertThatThrownBy(() -> membreService.genererModeleImport()) .isInstanceOf(RuntimeException.class) .hasMessageContaining("modèle"); } } // ========================================================================= // listerMembresParOrganisations (uses EntityManager — excluded from mock tests) // ========================================================================= @Test @DisplayName("listerMembresParOrganisations: liste null retourne liste vide") void listerMembresParOrganisations_null_returnsEmpty() { // This path doesn't touch EntityManager List result = membreService.listerMembresParOrganisations(null, Page.of(0, 10), Sort.by("nom")); assertThat(result).isEmpty(); } @Test @DisplayName("listerMembresParOrganisations: liste vide retourne liste vide") void listerMembresParOrganisations_empty_returnsEmpty() { List result = membreService.listerMembresParOrganisations(List.of(), Page.of(0, 10), Sort.by("nom")); assertThat(result).isEmpty(); } // ========================================================================= // obtenirVillesDistinctes / obtenirProfessionsDistinctes // (use EntityManager directly — available in @QuarkusTest context) // ========================================================================= @Test @DisplayName("obtenirVillesDistinctes sans query: retourne une liste (peut être vide)") void obtenirVillesDistinctes_noQuery_returnsList() { List villes = membreService.obtenirVillesDistinctes(null); assertThat(villes).isNotNull(); } @Test @DisplayName("obtenirVillesDistinctes avec query vide: retourne une liste") void obtenirVillesDistinctes_emptyQuery_returnsList() { List villes = membreService.obtenirVillesDistinctes(""); assertThat(villes).isNotNull(); } @Test @DisplayName("obtenirVillesDistinctes avec query: retourne une liste filtrée") void obtenirVillesDistinctes_withQuery_returnsList() { List villes = membreService.obtenirVillesDistinctes("Paris"); assertThat(villes).isNotNull(); } @Test @DisplayName("obtenirProfessionsDistinctes sans query: retourne une liste") void obtenirProfessionsDistinctes_noQuery_returnsList() { List professions = membreService.obtenirProfessionsDistinctes(null); assertThat(professions).isNotNull(); } @Test @DisplayName("obtenirProfessionsDistinctes avec query vide: retourne une liste") void obtenirProfessionsDistinctes_emptyQuery_returnsList() { List professions = membreService.obtenirProfessionsDistinctes(""); assertThat(professions).isNotNull(); } @Test @DisplayName("obtenirProfessionsDistinctes avec query: retourne une liste filtrée") void obtenirProfessionsDistinctes_withQuery_returnsList() { List professions = membreService.obtenirProfessionsDistinctes("Ingénieur"); assertThat(professions).isNotNull(); } // ========================================================================= // exporterVersCSV / exporterVersPDF / genererModeleImport — error branches // covered by ExportImportDelegationTests above; these cover listerMembres // ========================================================================= // ========================================================================= // listerMembresPourExport // ========================================================================= @Nested @DisplayName("listerMembresPourExport") class ListerMembresPourExportTests { @Test @DisplayName("sans associationId: liste tous les membres") void listerMembresPourExport_noAssociationId_listAll() { Membre m = membreFixture("export@test.dev"); m.setMembresOrganisations(new ArrayList<>()); when(membreRepository.listAll()).thenReturn(List.of(m)); when(membreRoleRepository.findActifsByMembreId(m.getId())).thenReturn(List.of()); List result = membreService.listerMembresPourExport(null, null, null, null, null); assertThat(result).hasSize(1); verify(membreRepository).listAll(); } @Test @DisplayName("filtre par statut ACTIF: garde seulement les membres actifs") void listerMembresPourExport_filtreActif() { Membre actif = membreFixture("actif@test.dev"); actif.setActif(true); actif.setMembresOrganisations(new ArrayList<>()); Membre inactif = membreFixture("inactif@test.dev"); inactif.setActif(false); inactif.setMembresOrganisations(new ArrayList<>()); when(membreRepository.listAll()).thenReturn(List.of(actif, inactif)); when(membreRoleRepository.findActifsByMembreId(actif.getId())).thenReturn(List.of()); List result = membreService.listerMembresPourExport(null, "ACTIF", null, null, null); assertThat(result).hasSize(1); assertThat(result.get(0).getEmail()).isEqualTo("actif@test.dev"); } @Test @DisplayName("filtre par statut INACTIF: garde seulement les membres inactifs") void listerMembresPourExport_filtreInactif() { Membre actif = membreFixture("actif2@test.dev"); actif.setActif(true); actif.setMembresOrganisations(new ArrayList<>()); Membre inactif = membreFixture("inactif2@test.dev"); inactif.setActif(false); inactif.setMembresOrganisations(new ArrayList<>()); when(membreRepository.listAll()).thenReturn(List.of(actif, inactif)); when(membreRoleRepository.findActifsByMembreId(inactif.getId())).thenReturn(List.of()); List result = membreService.listerMembresPourExport(null, "INACTIF", null, null, null); assertThat(result).hasSize(1); assertThat(result.get(0).getEmail()).isEqualTo("inactif2@test.dev"); } @Test @DisplayName("statut vide: tous les membres retournés sans filtre statut") void listerMembresPourExport_statutVide_noFilter() { Membre m1 = membreFixture("e1@test.dev"); m1.setActif(true); m1.setMembresOrganisations(new ArrayList<>()); Membre m2 = membreFixture("e2@test.dev"); m2.setActif(false); m2.setMembresOrganisations(new ArrayList<>()); when(membreRepository.listAll()).thenReturn(List.of(m1, m2)); when(membreRoleRepository.findActifsByMembreId(m1.getId())).thenReturn(List.of()); when(membreRoleRepository.findActifsByMembreId(m2.getId())).thenReturn(List.of()); List result = membreService.listerMembresPourExport(null, "", null, null, null); assertThat(result).hasSize(2); } } // ========================================================================= // importerMembres (delegation) // ========================================================================= @Test @DisplayName("importerMembres: délègue à membreImportExportService") void importerMembres_delegates() { java.io.InputStream is = new java.io.ByteArrayInputStream(new byte[0]); UUID orgId = UUID.randomUUID(); dev.lions.unionflow.server.service.MembreImportExportService.ResultatImport mockResult = new dev.lions.unionflow.server.service.MembreImportExportService.ResultatImport(); mockResult.totalLignes = 0; mockResult.lignesTraitees = 0; mockResult.lignesErreur = 0; mockResult.erreurs = new ArrayList<>(); when(membreImportExportService.importerMembres(is, "test.csv", orgId, "ACTIF", false, true)) .thenReturn(mockResult); dev.lions.unionflow.server.service.MembreImportExportService.ResultatImport result = membreService.importerMembres(is, "test.csv", orgId, "ACTIF", false, true); assertThat(result).isNotNull(); verify(membreImportExportService).importerMembres(is, "test.csv", orgId, "ACTIF", false, true); } // ========================================================================= // lierMembreOrganisationEtIncrementerQuota // ========================================================================= @Nested @DisplayName("lierMembreOrganisationEtIncrementerQuota") class LierMembreOrganisationTests { @Test @DisplayName("membre null: lève IllegalArgumentException") void lierMembre_nullMembre_throws() { assertThatThrownBy(() -> membreService.lierMembreOrganisationEtIncrementerQuota(null, UUID.randomUUID(), "ACTIF")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("obligatoires"); } @Test @DisplayName("organisationId null: lève IllegalArgumentException") void lierMembre_nullOrganisationId_throws() { Membre m = membreFixture("lier@test.dev"); assertThatThrownBy(() -> membreService.lierMembreOrganisationEtIncrementerQuota(m, null, "ACTIF")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("obligatoires"); } @Test @DisplayName("organisation introuvable: lève IllegalArgumentException") void lierMembre_organisationIntrouvable_throws() { Membre m = membreFixture("lier2@test.dev"); UUID orgId = UUID.randomUUID(); // UUID inexistant en base // EntityManager real + DB vide pour cet UUID → retourne null assertThatThrownBy(() -> membreService.lierMembreOrganisationEtIncrementerQuota(m, orgId, "ACTIF")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Organisation non trouvée"); } @Test @DisplayName("typeMembreDefaut non-ACTIF: statut EN_ATTENTE_VALIDATION") void lierMembre_typeNonActif_statutEnAttente() { // Vérification que le chemin EN_ATTENTE_VALIDATION est bien sélectionné // via l'enum (le test de l'organisation introuvable intervient après) Membre m = membreFixture("lier3@test.dev"); UUID orgId = UUID.randomUUID(); assertThatThrownBy(() -> membreService.lierMembreOrganisationEtIncrementerQuota(m, orgId, "EN_ATTENTE_VALIDATION")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Organisation non trouvée"); } } // ========================================================================= // searchMembresAdvanced — branches ADMIN_ORGANISATION // ========================================================================= @Nested @DisplayName("searchMembresAdvanced — branches sécurité") class SearchMembresAdvancedSecurityTests { @Test @DisplayName("ADMIN_ORGANISATION sans organisations: retourne résultat vide") void searchMembresAdvanced_adminOrg_noOrgs_returnsEmpty() { dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria = dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder().build(); Principal principal = () -> "admin@org.dev"; when(securityIdentity.getPrincipal()).thenReturn(principal); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION")); when(organisationService.listerOrganisationsPourUtilisateur("admin@org.dev")) .thenReturn(List.of()); dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result = membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom")); assertThat(result).isNotNull(); assertThat(result.getTotalElements()).isEqualTo(0L); } @Test @DisplayName("ADMIN_ORGANISATION avec organisations: restreint par orgIds") void searchMembresAdvanced_adminOrg_withOrgs_restrictsToOrgs() { dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria = dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder().build(); UUID orgId = UUID.randomUUID(); dev.lions.unionflow.server.entity.Organisation org = new dev.lions.unionflow.server.entity.Organisation(); org.setId(orgId); Principal principal = () -> "admin@org.dev"; when(securityIdentity.getPrincipal()).thenReturn(principal); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION")); when(organisationService.listerOrganisationsPourUtilisateur("admin@org.dev")) .thenReturn(List.of(org)); // Appelle le vrai EntityManager (DB vide → totalElements = 0) dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result = membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom")); assertThat(result).isNotNull(); } @Test @DisplayName("ADMIN_ORGANISATION avec organisationIds en intersection: filtre correctement") void searchMembresAdvanced_adminOrg_intersectionOrgIds() { UUID orgId1 = UUID.randomUUID(); UUID orgId2 = UUID.randomUUID(); dev.lions.unionflow.server.entity.Organisation org1 = new dev.lions.unionflow.server.entity.Organisation(); org1.setId(orgId1); dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria = dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder() .organisationIds(new ArrayList<>(List.of(orgId2))) .build(); Principal principal = () -> "admin@org.dev"; when(securityIdentity.getPrincipal()).thenReturn(principal); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION")); when(organisationService.listerOrganisationsPourUtilisateur("admin@org.dev")) .thenReturn(List.of(org1)); // L'intersection est vide → totalElements = 0 dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result = membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom")); assertThat(result).isNotNull(); } @Test @DisplayName("Utilisateur normal: recherche sans restriction") void searchMembresAdvanced_normalUser_noRestriction() { dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria = dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder().build(); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN")); // Appelle le vrai EntityManager (DB vide ou non → pas d'exception) dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result = membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom")); assertThat(result).isNotNull(); } @Test @DisplayName("Critères avec statut ACTIF: filtre par actif=true") void searchMembresAdvanced_avecStatutActif() { dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria = dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder() .statut("ACTIF") .build(); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN")); dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result = membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom")); assertThat(result).isNotNull(); } @Test @DisplayName("Critères avec includeInactifs: inclut tous") void searchMembresAdvanced_avecIncludeInactifs() { dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria = dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder() .includeInactifs(true) .build(); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN")); dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result = membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom")); assertThat(result).isNotNull(); } @Test @DisplayName("Critères avec statut non-ACTIF: inclut inactifs") void searchMembresAdvanced_avecStatutInactif() { dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria = dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder() .statut("INACTIF") .build(); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN")); dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result = membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom")); assertThat(result).isNotNull(); } @Test @DisplayName("Critères avec query, nom, prenom, email, telephone: construit la requête JPQL correctement") void searchMembresAdvanced_avecTousCriteres() { dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria = dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder() .query("test") .nom("Doe") .prenom("John") .email("john@test.dev") .telephone("0600") .build(); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN")); dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result = membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom")); assertThat(result).isNotNull(); } @Test @DisplayName("Sort avec direction Descending: clause ORDER BY contient DESC") void searchMembresAdvanced_sortDescending_construitClauseDesc() { dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria = dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder().build(); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN")); // Sort.by("nom").descending() crée une colonne Direction.Descending → couvre la branche DESC dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result = membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom").descending()); assertThat(result).isNotNull(); } @Test @DisplayName("Sort null: searchMembresAdvanced ne lève pas d'exception") void searchMembresAdvanced_sortNull_pasDException() { dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria = dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder().build(); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN")); // null sort → buildOrderByClause retourne "m.nom ASC" (branche default) dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result = membreService.searchMembresAdvanced(criteria, Page.of(0, 10), null); assertThat(result).isNotNull(); } @Test @DisplayName("Sort avec colonnes vides: buildOrderByClause retourne 'm.nom ASC' (branche isEmpty)") void searchMembresAdvanced_sortEmptyColumns_coversEmptyBranch() throws Exception { dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria = dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder().build(); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN")); // Sort non-null mais avec colonnes vides → branche sort.getColumns().isEmpty() Sort emptySort; try { java.lang.reflect.Constructor ctor = Sort.class.getDeclaredConstructor(); ctor.setAccessible(true); emptySort = ctor.newInstance(); } catch (NoSuchMethodException e) { return; } dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result = membreService.searchMembresAdvanced(criteria, Page.of(0, 10), emptySort); assertThat(result).isNotNull(); } } // ========================================================================= // listerMembresParOrganisations — avec liste non vide (EntityManager réel) // ========================================================================= @Test @DisplayName("listerMembresParOrganisations: UUID inexistant → liste vide (aucun membre lié)") void listerMembresParOrganisations_uuidInexistant_listeVide() { // Passe une liste non vide avec un UUID inexistant → la requête JPQL retourne [] List result = membreService.listerMembresParOrganisations( List.of(UUID.randomUUID()), Page.of(0, 10), Sort.by("nom")); assertThat(result).isNotNull(); assertThat(result).isEmpty(); } @Test @DisplayName("listerMembresParOrganisations: sans pagination (page null) → ne lève pas d'exception") void listerMembresParOrganisations_pagingNull_pasDException() { List result = membreService.listerMembresParOrganisations( List.of(UUID.randomUUID()), null, null); assertThat(result).isNotNull(); } // ========================================================================= // listerMembresPourExport — branche associationId != null // ========================================================================= @Test @DisplayName("listerMembresPourExport avec associationId inexistant: retourne liste vide") void listerMembresPourExport_avecAssociationId_listeVide() { // EntityManager réel disponible dans @QuarkusTest — UUID inexistant → liste vide List result = membreService.listerMembresPourExport(UUID.randomUUID(), null, null, null, null); assertThat(result).isNotNull(); assertThat(result).isEmpty(); } @Test @DisplayName("listerMembresPourExport avec associationId et filtre ACTIF: filtre les membres") void listerMembresPourExport_avecAssociationIdEtFiltreActif_resultatFiltre() { UUID orgId = UUID.randomUUID(); // inexistant → 0 membres List result = membreService.listerMembresPourExport(orgId, "ACTIF", null, null, null); assertThat(result).isNotNull(); } // ========================================================================= // Tests @TestTransaction (doivent être dans la classe externe — CDI interdit // les interceptor bindings sur les méthodes des classes internes JUnit @Nested) // ========================================================================= @Test @TestTransaction @DisplayName("mettreAJourMembre: champs mis à jour correctement (même email)") void mettreAJourMembre_sameEmail_updatesFields() { // Persist without pre-set ID — let Hibernate generate it (@GeneratedValue UUID) Membre existing = new Membre(); existing.setEmail("same@unionflow.dev"); existing.setNom("Doe"); existing.setPrenom("John"); existing.setDateNaissance(LocalDate.of(1990, 1, 1)); existing.setNumeroMembre("UF2024-SAME"); existing.setActif(true); existing.setStatutCompte("ACTIF"); existing.setMembresOrganisations(new ArrayList<>()); existing.setAdresses(new ArrayList<>()); em.persist(existing); em.flush(); UUID id = existing.getId(); Membre modifie = new Membre(); modifie.setEmail("same@unionflow.dev"); modifie.setNom("Smith"); modifie.setPrenom("Jane"); modifie.setTelephone("0600000001"); modifie.setDateNaissance(LocalDate.of(1992, 3, 10)); modifie.setActif(false); Membre updated = membreService.mettreAJourMembre(id, modifie); assertThat(updated.getNom()).isEqualTo("Smith"); assertThat(updated.getPrenom()).isEqualTo("Jane"); assertThat(updated.getTelephone()).isEqualTo("0600000001"); assertThat(updated.getActif()).isFalse(); } @Test @TestTransaction @DisplayName("mettreAJourMembre: email modifié, nouveau email disponible") void mettreAJourMembre_newEmailAvailable_updatesEmail() { Membre existing = new Membre(); existing.setEmail("old@unionflow.dev"); existing.setNom("Doe"); existing.setPrenom("John"); existing.setDateNaissance(LocalDate.of(1990, 1, 1)); existing.setNumeroMembre("UF2024-OLD"); existing.setActif(true); existing.setStatutCompte("ACTIF"); existing.setMembresOrganisations(new ArrayList<>()); existing.setAdresses(new ArrayList<>()); em.persist(existing); em.flush(); UUID id = existing.getId(); Membre modifie = new Membre(); modifie.setEmail("new@unionflow.dev"); modifie.setNom("Smith"); modifie.setPrenom("Jane"); modifie.setDateNaissance(LocalDate.of(1990, 1, 1)); modifie.setActif(true); Membre updated = membreService.mettreAJourMembre(id, modifie); assertThat(updated.getEmail()).isEqualTo("new@unionflow.dev"); } @Test @TestTransaction @DisplayName("mettreAJourMembre: email modifié mais déjà pris → IllegalArgumentException") void mettreAJourMembre_emailAlreadyTaken_throws() { Membre existing = new Membre(); existing.setEmail("old2@unionflow.dev"); existing.setNom("Doe"); existing.setPrenom("John"); existing.setDateNaissance(LocalDate.of(1990, 1, 1)); existing.setNumeroMembre("UF2024-OLD2"); existing.setActif(true); existing.setStatutCompte("ACTIF"); existing.setMembresOrganisations(new ArrayList<>()); existing.setAdresses(new ArrayList<>()); em.persist(existing); em.flush(); UUID id = existing.getId(); Membre modifie = new Membre(); modifie.setEmail("taken@unionflow.dev"); modifie.setNom("Smith"); modifie.setPrenom("Jane"); modifie.setDateNaissance(LocalDate.of(1990, 1, 1)); when(membreRepository.findByEmail("taken@unionflow.dev")) .thenReturn(Optional.of(membreFixture("taken@unionflow.dev"))); assertThatThrownBy(() -> membreService.mettreAJourMembre(id, modifie)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("email existe déjà"); } @Test @TestTransaction @DisplayName("trouverParId: retourne le membre si trouvé") void trouverParId_found() { Membre m = new Membre(); m.setEmail("find@unionflow.dev"); m.setNom("Doe"); m.setPrenom("John"); m.setDateNaissance(LocalDate.of(1990, 1, 1)); m.setNumeroMembre("UF2024-FIND"); m.setActif(true); m.setStatutCompte("ACTIF"); m.setMembresOrganisations(new ArrayList<>()); m.setAdresses(new ArrayList<>()); em.persist(m); em.flush(); UUID generatedId = m.getId(); Optional result = membreService.trouverParId(generatedId); assertThat(result).isPresent(); assertThat(result.get().getEmail()).isEqualTo("find@unionflow.dev"); } @Test @TestTransaction @DisplayName("desactiverMembre: flag actif mis à false") void desactiverMembre_setsActifFalse() { Membre existing = new Membre(); existing.setEmail("active@unionflow.dev"); existing.setNom("Doe"); existing.setPrenom("John"); existing.setDateNaissance(LocalDate.of(1990, 1, 1)); existing.setNumeroMembre("UF2024-ACTIVE"); existing.setActif(true); existing.setStatutCompte("ACTIF"); existing.setMembresOrganisations(new ArrayList<>()); existing.setAdresses(new ArrayList<>()); em.persist(existing); em.flush(); UUID id = existing.getId(); membreService.desactiverMembre(id); // existing is the managed entity — service modifies the same instance assertThat(existing.getActif()).isFalse(); } // ========================================================================= // getOrganisationIdsForCurrentUserIfAdminOrg — branches manquantes // ========================================================================= @Test @DisplayName("listerMembres avec SUPER_ADMIN + ADMIN_ORGANISATION: pas de restriction (branche SUPER_ADMIN couverte)") void listerMembres_superAdminAndAdminOrg_returnsAll() { Page page = Page.of(0, 10); Sort sort = Sort.by("nom").ascending(); List all = List.of(membreFixture("super@test.dev")); Principal principal = () -> "super@org.dev"; when(securityIdentity.getPrincipal()).thenReturn(principal); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION", "SUPER_ADMIN")); when(membreRepository.findAll(page, sort)).thenReturn(all); List result = membreService.listerMembres(page, sort); assertThat(result).hasSize(1); } @Test @DisplayName("listerMembres avec ADMIN_ORGANISATION + email null depuis principal: retourne tous les membres") void listerMembres_adminOrg_nullEmail_returnsAll() { Page page = Page.of(0, 10); Sort sort = Sort.by("nom").ascending(); List all = List.of(membreFixture("n@test.dev")); // Principal whose getName() returns null Principal principal = () -> null; when(securityIdentity.getPrincipal()).thenReturn(principal); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION")); when(membreRepository.findAll(page, sort)).thenReturn(all); List result = membreService.listerMembres(page, sort); assertThat(result).hasSize(1); } // ========================================================================= // searchMembresAdvanced + buildOrderByClause — branche Descending avec données réelles // Nécessite totalElements > 0 pour que buildOrderByClause soit appelé // ========================================================================= @Test @TestTransaction @DisplayName("searchMembresAdvanced avec données réelles et sort Descending couvre buildOrderByClause DESC") void searchMembresAdvanced_avecDonnees_sortDescending_couvreDescBranch() { // Persister un membre pour que totalElements > 0 Membre m = new Membre(); m.setEmail("search-desc-" + UUID.randomUUID() + "@test.com"); m.setNom("Zorro"); m.setPrenom("Test"); m.setDateNaissance(LocalDate.of(1990, 1, 1)); m.setNumeroMembre("UF-DESC-" + UUID.randomUUID().toString().substring(0, 8)); m.setActif(true); m.setStatutCompte("ACTIF"); m.setMembresOrganisations(new ArrayList<>()); m.setAdresses(new ArrayList<>()); em.persist(m); em.flush(); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN")); dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria = dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder().build(); // Sort Descending → totalElements > 0 → buildOrderByClause appelé avec Descending → branche DESC couverte dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result = membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom").descending()); assertThat(result).isNotNull(); assertThat(result.getTotalElements()).isGreaterThan(0L); } @Test @TestTransaction @DisplayName("searchMembresAdvanced avec données réelles et sort colonnes vides couvre buildOrderByClause isEmpty()") void searchMembresAdvanced_avecDonnees_sortColonnesVides_couvreIsEmptyBranch() throws Exception { // Persister un membre pour que totalElements > 0 → buildOrderByClause est appelé Membre m = new Membre(); m.setEmail("search-empty-sort-" + UUID.randomUUID() + "@test.com"); m.setNom("Alpha"); m.setPrenom("Test"); m.setDateNaissance(LocalDate.of(1990, 1, 1)); m.setNumeroMembre("UF-EMPTY-" + UUID.randomUUID().toString().substring(0, 8)); m.setActif(true); m.setStatutCompte("ACTIF"); m.setMembresOrganisations(new ArrayList<>()); m.setAdresses(new ArrayList<>()); em.persist(m); em.flush(); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN")); dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria = dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder().build(); // Sort non-null mais colonnes vides → buildOrderByClause appelé → branche isEmpty() couverte Sort emptySort; try { java.lang.reflect.Constructor ctor = Sort.class.getDeclaredConstructor(); ctor.setAccessible(true); emptySort = ctor.newInstance(); } catch (NoSuchMethodException e) { // Si le constructeur n'est pas accessible, le test passe sans erreur return; } dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result = membreService.searchMembresAdvanced(criteria, Page.of(0, 10), emptySort); assertThat(result).isNotNull(); assertThat(result.getTotalElements()).isGreaterThan(0L); } // ========================================================================= // creerMembre — branche getNumeroMembre().isEmpty() = true (non-null mais vide) // ========================================================================= @Test @TestTransaction @DisplayName("creerMembre avec numeroMembre vide ('') → génère un nouveau numéro (branche isEmpty()=true)") void creerMembre_emptyNumeroMembre_generatesNew() { Membre m = new Membre(); m.setEmail("creer-empty-num-" + UUID.randomUUID() + "@test.dev"); m.setNom("Vide"); m.setPrenom("Num"); m.setDateNaissance(LocalDate.of(1990, 1, 1)); m.setNumeroMembre(""); // non-null mais vide → isEmpty()=true → génère un nouveau numéro m.setActif(true); m.setStatutCompte("ACTIF"); m.setMembresOrganisations(new ArrayList<>()); m.setAdresses(new ArrayList<>()); Membre created = membreService.creerMembre(m); assertThat(created).isNotNull(); assertThat(created.getNumeroMembre()).isNotNull().isNotEmpty(); } // ========================================================================= // getOrganisationIdsForCurrentUserIfAdminOrg — branche email.isBlank()=true // ========================================================================= @Test @DisplayName("listerMembres avec ADMIN_ORGANISATION + email blanc depuis principal → retourne tous (branche isBlank=true L224)") void listerMembres_adminOrg_blankEmail_returnsAll() { Page page = Page.of(0, 10); Sort sort = Sort.by("nom").ascending(); List all = List.of(membreFixture("blank-email@test.dev")); // Principal dont getName() retourne une chaîne vide → isBlank()=true Principal principal = () -> " "; when(securityIdentity.getPrincipal()).thenReturn(principal); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION")); when(membreRepository.findAll(page, sort)).thenReturn(all); List result = membreService.listerMembres(page, sort); assertThat(result).hasSize(1); } // ========================================================================= // getOrganisationIdsForCurrentUserIfAdminOrg — branche orgs == null (L226) // ========================================================================= @Test @DisplayName("listerMembres avec ADMIN_ORGANISATION + service retourne null → retourne liste vide (branche orgs==null L226)") void listerMembres_adminOrg_nullOrgsList_returnsEmpty() { Page page = Page.of(0, 10); Sort sort = Sort.by("nom").ascending(); Principal principal = () -> "admin-null-orgs@org.dev"; when(securityIdentity.getPrincipal()).thenReturn(principal); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION")); // listerOrganisationsPourUtilisateur retourne null → orgs == null = true → Optional.of(Set.of()) when(organisationService.listerOrganisationsPourUtilisateur("admin-null-orgs@org.dev")) .thenReturn(null); List result = membreService.listerMembres(page, sort); assertThat(result).isEmpty(); } // ========================================================================= // addSearchCriteria — branche getStatut()==null && includeInactifs==true (L595) // Couvre la branche "else if(!includeInactifs) = false" → rien n'est ajouté // ========================================================================= @Test @TestTransaction @DisplayName("searchMembresAdvanced avec includeInactifs=true et statut=null → pas de filtre actif (branche else if false)") void searchMembresAdvanced_includeInactifsTrue_noActifFilter() { // Persister un membre inactif Membre m = new Membre(); m.setEmail("search-inactif-" + UUID.randomUUID() + "@test.com"); m.setNom("Inactif"); m.setPrenom("Test"); m.setDateNaissance(LocalDate.of(1990, 1, 1)); m.setNumeroMembre("UF-INACT-" + UUID.randomUUID().toString().substring(0, 8)); m.setActif(false); // inactif m.setStatutCompte("INACTIF"); m.setMembresOrganisations(new ArrayList<>()); m.setAdresses(new ArrayList<>()); em.persist(m); em.flush(); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN")); // includeInactifs=true, statut=null → condition `else if(!includeInactifs)` = false → pas de filtre actif dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria = dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder() .includeInactifs(true) .build(); dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result = membreService.searchMembresAdvanced(criteria, Page.of(0, 100), Sort.by("nom").ascending()); assertThat(result).isNotNull(); // Les membres inactifs sont inclus assertThat(result.getTotalElements()).isGreaterThan(0L); } // ========================================================================= // lambda$calculateSearchStatistics$11:710 — branche r != null && !r.isEmpty() false (r = "") // ========================================================================= @Test @TestTransaction @DisplayName("calculateSearchStatistics avec région vide ('') couvre la branche r != null && !r.isEmpty() = false") void searchMembresAdvanced_regionVide_brancheFalse() { // Persister un membre avec une adresse dont la région est une chaîne vide Membre m = new Membre(); m.setEmail("search-region-empty-" + UUID.randomUUID() + "@test.com"); m.setNom("RegionVide"); m.setPrenom("Test"); m.setDateNaissance(LocalDate.of(1990, 1, 1)); m.setNumeroMembre("UF-REGV-" + UUID.randomUUID().toString().substring(0, 8)); m.setActif(true); m.setStatutCompte("ACTIF"); m.setMembresOrganisations(new ArrayList<>()); // Adresse avec région = "" (non-null mais vide → !r.isEmpty() = false → filtrée) dev.lions.unionflow.server.entity.Adresse adresse = new dev.lions.unionflow.server.entity.Adresse(); adresse.setRegion(""); // chaîne vide → filtrée par lambda adresse.setTypeAdresse("DOMICILE"); // NOT NULL constraint adresse.setMembre(m); m.setAdresses(List.of(adresse)); em.persist(m); em.flush(); when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN")); dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria criteria = dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria.builder().build(); dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO result = membreService.searchMembresAdvanced(criteria, Page.of(0, 100), Sort.by("nom").ascending()); assertThat(result).isNotNull(); assertThat(result.getTotalElements()).isGreaterThan(0L); } }