diff --git a/src/test/java/dev/lions/unionflow/server/entity/ContactPolicyTest.java b/src/test/java/dev/lions/unionflow/server/entity/ContactPolicyTest.java new file mode 100644 index 0000000..4817ebd --- /dev/null +++ b/src/test/java/dev/lions/unionflow/server/entity/ContactPolicyTest.java @@ -0,0 +1,109 @@ +package dev.lions.unionflow.server.entity; + +import dev.lions.unionflow.server.api.enums.messagerie.TypePolitiqueCommunication; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("ContactPolicy") +class ContactPolicyTest { + + private static Organisation newOrg() { + Organisation org = new Organisation(); + org.setId(UUID.randomUUID()); + org.setNom("Org Test"); + return org; + } + + @Test + @DisplayName("getters/setters — tous les champs") + void gettersSetters() { + ContactPolicy policy = new ContactPolicy(); + Organisation org = newOrg(); + policy.setOrganisation(org); + policy.setTypePolitique(TypePolitiqueCommunication.BUREAU_SEULEMENT); + policy.setAutoriserMembreVersMembre(false); + policy.setAutoriserMembreVersRole(true); + policy.setAutoriserNotesVocales(false); + + assertThat(policy.getOrganisation()).isEqualTo(org); + assertThat(policy.getTypePolitique()).isEqualTo(TypePolitiqueCommunication.BUREAU_SEULEMENT); + assertThat(policy.getAutoriserMembreVersMembre()).isFalse(); + assertThat(policy.getAutoriserMembreVersRole()).isTrue(); + assertThat(policy.getAutoriserNotesVocales()).isFalse(); + } + + @Test + @DisplayName("onCreate — valeurs par défaut : OUVERT, tout à true") + void onCreate_defaults() { + ContactPolicy policy = new ContactPolicy(); + policy.setOrganisation(newOrg()); + + policy.onCreate(); + + assertThat(policy.getTypePolitique()).isEqualTo(TypePolitiqueCommunication.OUVERT); + assertThat(policy.getAutoriserMembreVersMembre()).isTrue(); + assertThat(policy.getAutoriserMembreVersRole()).isTrue(); + assertThat(policy.getAutoriserNotesVocales()).isTrue(); + assertThat(policy.getDateCreation()).isNotNull(); + assertThat(policy.getActif()).isTrue(); + } + + @Test + @DisplayName("onCreate — ne remplace pas les valeurs existantes") + void onCreate_preservesExisting() { + ContactPolicy policy = new ContactPolicy(); + policy.setOrganisation(newOrg()); + policy.setTypePolitique(TypePolitiqueCommunication.GROUPES_INTERNES); + policy.setAutoriserNotesVocales(false); + + policy.onCreate(); + + assertThat(policy.getTypePolitique()).isEqualTo(TypePolitiqueCommunication.GROUPES_INTERNES); + assertThat(policy.getAutoriserNotesVocales()).isFalse(); + } + + @Test + @DisplayName("builder — tous les champs") + void builder_allFields() { + Organisation org = newOrg(); + ContactPolicy policy = ContactPolicy.builder() + .organisation(org) + .typePolitique(TypePolitiqueCommunication.OUVERT) + .autoriserMembreVersMembre(true) + .autoriserMembreVersRole(true) + .autoriserNotesVocales(true) + .build(); + + assertThat(policy.getOrganisation()).isEqualTo(org); + assertThat(policy.getTypePolitique()).isEqualTo(TypePolitiqueCommunication.OUVERT); + assertThat(policy.getAutoriserNotesVocales()).isTrue(); + } + + @Test + @DisplayName("equals et hashCode") + void equalsHashCode() { + UUID id = UUID.randomUUID(); + Organisation org = newOrg(); + + ContactPolicy a = ContactPolicy.builder().organisation(org) + .typePolitique(TypePolitiqueCommunication.OUVERT).build(); + a.setId(id); + ContactPolicy b = ContactPolicy.builder().organisation(org) + .typePolitique(TypePolitiqueCommunication.OUVERT).build(); + b.setId(id); + + assertThat(a).isEqualTo(b); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + } + + @Test + @DisplayName("toString non null") + void toString_nonNull() { + ContactPolicy policy = ContactPolicy.builder().organisation(newOrg()).build(); + assertThat(policy.toString()).isNotNull().isNotEmpty(); + } +} diff --git a/src/test/java/dev/lions/unionflow/server/entity/ConversationParticipantTest.java b/src/test/java/dev/lions/unionflow/server/entity/ConversationParticipantTest.java new file mode 100644 index 0000000..b822278 --- /dev/null +++ b/src/test/java/dev/lions/unionflow/server/entity/ConversationParticipantTest.java @@ -0,0 +1,128 @@ +package dev.lions.unionflow.server.entity; + +import dev.lions.unionflow.server.api.enums.messagerie.StatutConversation; +import dev.lions.unionflow.server.api.enums.messagerie.TypeConversation; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("ConversationParticipant") +class ConversationParticipantTest { + + private static Membre newMembre() { + Membre m = new Membre(); + m.setId(UUID.randomUUID()); + m.setNumeroMembre("M1"); + m.setPrenom("Alpha"); + m.setNom("Diallo"); + m.setEmail("alpha@test.com"); + m.setDateNaissance(LocalDate.now()); + return m; + } + + private static Conversation newConversation() { + Organisation org = new Organisation(); + org.setId(UUID.randomUUID()); + org.setNom("Org Test"); + Conversation c = new Conversation(); + c.setId(UUID.randomUUID()); + c.setOrganisation(org); + c.setTypeConversation(TypeConversation.DIRECTE); + c.setStatut(StatutConversation.ACTIVE); + c.setNombreMessages(0); + return c; + } + + @Test + @DisplayName("getters/setters") + void gettersSetters() { + ConversationParticipant p = new ConversationParticipant(); + Conversation conv = newConversation(); + Membre membre = newMembre(); + p.setConversation(conv); + p.setMembre(membre); + p.setRoleDansConversation("INITIATEUR"); + p.setNotifier(true); + + assertThat(p.getConversation()).isEqualTo(conv); + assertThat(p.getMembre()).isEqualTo(membre); + assertThat(p.getRoleDansConversation()).isEqualTo("INITIATEUR"); + assertThat(p.getNotifier()).isTrue(); + } + + @Test + @DisplayName("estInitiateur — INITIATEUR → true") + void estInitiateur_true() { + ConversationParticipant p = buildMinimal("INITIATEUR"); + assertThat(p.estInitiateur()).isTrue(); + } + + @Test + @DisplayName("estInitiateur — PARTICIPANT → false") + void estInitiateur_false() { + ConversationParticipant p = buildMinimal("PARTICIPANT"); + assertThat(p.estInitiateur()).isFalse(); + } + + @Test + @DisplayName("marquerLu — luJusqua mis à jour") + void marquerLu() { + ConversationParticipant p = buildMinimal("PARTICIPANT"); + p.setLuJusqua(null); + p.marquerLu(); + assertThat(p.getLuJusqua()).isNotNull(); + } + + @Test + @DisplayName("marquerLu — luJusqua remplace l'ancien") + void marquerLu_replaceOld() { + ConversationParticipant p = buildMinimal("PARTICIPANT"); + LocalDateTime old = LocalDateTime.now().minusDays(1); + p.setLuJusqua(old); + p.marquerLu(); + assertThat(p.getLuJusqua()).isAfter(old); + } + + @Test + @DisplayName("builder — valeurs par défaut") + void builder_defaults() { + ConversationParticipant p = ConversationParticipant.builder() + .conversation(newConversation()) + .membre(newMembre()) + .build(); + assertThat(p.getRoleDansConversation()).isEqualTo("PARTICIPANT"); + assertThat(p.getNotifier()).isTrue(); + } + + @Test + @DisplayName("equals et hashCode") + void equalsHashCode() { + UUID id = UUID.randomUUID(); + ConversationParticipant a = buildMinimal("PARTICIPANT"); + a.setId(id); + ConversationParticipant b = buildMinimal("PARTICIPANT"); + b.setId(id); + assertThat(a).isEqualTo(b); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + } + + @Test + @DisplayName("toString non null") + void toString_nonNull() { + assertThat(buildMinimal("PARTICIPANT").toString()).isNotNull().isNotEmpty(); + } + + private ConversationParticipant buildMinimal(String role) { + ConversationParticipant p = new ConversationParticipant(); + p.setConversation(newConversation()); + p.setMembre(newMembre()); + p.setRoleDansConversation(role); + p.setNotifier(true); + return p; + } +} diff --git a/src/test/java/dev/lions/unionflow/server/entity/MemberBlockTest.java b/src/test/java/dev/lions/unionflow/server/entity/MemberBlockTest.java new file mode 100644 index 0000000..77048d5 --- /dev/null +++ b/src/test/java/dev/lions/unionflow/server/entity/MemberBlockTest.java @@ -0,0 +1,94 @@ +package dev.lions.unionflow.server.entity; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("MemberBlock") +class MemberBlockTest { + + private static Membre newMembre(String email) { + Membre m = new Membre(); + m.setId(UUID.randomUUID()); + m.setNumeroMembre("M-" + UUID.randomUUID().toString().substring(0, 4)); + m.setPrenom("Test"); + m.setNom("User"); + m.setEmail(email); + m.setDateNaissance(LocalDate.now()); + return m; + } + + private static Organisation newOrg() { + Organisation org = new Organisation(); + org.setId(UUID.randomUUID()); + org.setNom("Org Test"); + return org; + } + + @Test + @DisplayName("getters/setters") + void gettersSetters() { + Membre bloqueur = newMembre("bloqueur@test.com"); + Membre bloque = newMembre("bloque@test.com"); + Organisation org = newOrg(); + + MemberBlock block = new MemberBlock(); + block.setBloqueur(bloqueur); + block.setBloque(bloque); + block.setOrganisation(org); + + assertThat(block.getBloqueur()).isEqualTo(bloqueur); + assertThat(block.getBloque()).isEqualTo(bloque); + assertThat(block.getOrganisation()).isEqualTo(org); + } + + @Test + @DisplayName("builder — tous les champs") + void builder_allFields() { + Membre bloqueur = newMembre("a@test.com"); + Membre bloque = newMembre("b@test.com"); + Organisation org = newOrg(); + + MemberBlock block = MemberBlock.builder() + .bloqueur(bloqueur) + .bloque(bloque) + .organisation(org) + .build(); + + assertThat(block.getBloqueur()).isEqualTo(bloqueur); + assertThat(block.getBloque()).isEqualTo(bloque); + assertThat(block.getOrganisation()).isEqualTo(org); + } + + @Test + @DisplayName("equals et hashCode") + void equalsHashCode() { + UUID id = UUID.randomUUID(); + Membre bloqueur = newMembre("a@test.com"); + Membre bloque = newMembre("b@test.com"); + Organisation org = newOrg(); + + MemberBlock a = MemberBlock.builder().bloqueur(bloqueur).bloque(bloque).organisation(org).build(); + a.setId(id); + MemberBlock b = MemberBlock.builder().bloqueur(bloqueur).bloque(bloque).organisation(org).build(); + b.setId(id); + + assertThat(a).isEqualTo(b); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + } + + @Test + @DisplayName("toString non null") + void toString_nonNull() { + MemberBlock block = MemberBlock.builder() + .bloqueur(newMembre("a@test.com")) + .bloque(newMembre("b@test.com")) + .organisation(newOrg()) + .build(); + assertThat(block.toString()).isNotNull().isNotEmpty(); + } +} diff --git a/src/test/java/dev/lions/unionflow/server/entity/VersementObjetTest.java b/src/test/java/dev/lions/unionflow/server/entity/VersementObjetTest.java new file mode 100644 index 0000000..d808754 --- /dev/null +++ b/src/test/java/dev/lions/unionflow/server/entity/VersementObjetTest.java @@ -0,0 +1,108 @@ +package dev.lions.unionflow.server.entity; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("VersementObjet") +class VersementObjetTest { + + private static Versement newVersement() { + Membre m = new Membre(); + m.setId(UUID.randomUUID()); + m.setNumeroMembre("M1"); + m.setPrenom("A"); + m.setNom("B"); + m.setEmail("a@test.com"); + m.setDateNaissance(java.time.LocalDate.now()); + Versement v = new Versement(); + v.setId(UUID.randomUUID()); + v.setNumeroReference("VRS-2026-001"); + v.setMontant(BigDecimal.TEN); + v.setCodeDevise("XOF"); + v.setMethodePaiement("WAVE"); + v.setMembre(m); + return v; + } + + @Test + @DisplayName("getters/setters") + void gettersSetters() { + VersementObjet vo = new VersementObjet(); + vo.setVersement(newVersement()); + vo.setTypeObjetCible("COTISATION"); + vo.setObjetCibleId(UUID.randomUUID()); + vo.setMontantApplique(new BigDecimal("5000.00")); + vo.setDateApplication(LocalDateTime.now()); + vo.setCommentaire("Cotisation janvier"); + + assertThat(vo.getTypeObjetCible()).isEqualTo("COTISATION"); + assertThat(vo.getMontantApplique()).isEqualByComparingTo("5000.00"); + assertThat(vo.getCommentaire()).isEqualTo("Cotisation janvier"); + } + + @Test + @DisplayName("equals et hashCode") + void equalsHashCode() { + UUID id = UUID.randomUUID(); + UUID objId = UUID.randomUUID(); + Versement v = newVersement(); + VersementObjet a = new VersementObjet(); + a.setId(id); + a.setVersement(v); + a.setTypeObjetCible("COTISATION"); + a.setObjetCibleId(objId); + a.setMontantApplique(BigDecimal.ONE); + VersementObjet b = new VersementObjet(); + b.setId(id); + b.setVersement(v); + b.setTypeObjetCible("COTISATION"); + b.setObjetCibleId(objId); + b.setMontantApplique(BigDecimal.ONE); + assertThat(a).isEqualTo(b); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + } + + @Test + @DisplayName("toString non null") + void toString_nonNull() { + VersementObjet vo = new VersementObjet(); + vo.setVersement(newVersement()); + vo.setTypeObjetCible("COTISATION"); + vo.setObjetCibleId(UUID.randomUUID()); + vo.setMontantApplique(BigDecimal.ONE); + assertThat(vo.toString()).isNotNull().isNotEmpty(); + } + + @Test + @DisplayName("onCreate initialise dateApplication si null") + void onCreate_setsDateApplicationWhenNull() { + VersementObjet vo = new VersementObjet(); + vo.setVersement(newVersement()); + vo.setTypeObjetCible("COTISATION"); + vo.setObjetCibleId(UUID.randomUUID()); + vo.setMontantApplique(BigDecimal.ONE); + + vo.onCreate(); + + assertThat(vo.getDateApplication()).isNotNull(); + assertThat(vo.getActif()).isTrue(); + } + + @Test + @DisplayName("onCreate ne remplace pas une dateApplication existante") + void onCreate_doesNotOverrideDateApplication() { + LocalDateTime fixed = LocalDateTime.of(2026, 1, 1, 0, 0); + VersementObjet vo = new VersementObjet(); + vo.setDateApplication(fixed); + + vo.onCreate(); + + assertThat(vo.getDateApplication()).isEqualTo(fixed); + } +} diff --git a/src/test/java/dev/lions/unionflow/server/entity/VersementTest.java b/src/test/java/dev/lions/unionflow/server/entity/VersementTest.java new file mode 100644 index 0000000..d033852 --- /dev/null +++ b/src/test/java/dev/lions/unionflow/server/entity/VersementTest.java @@ -0,0 +1,160 @@ +package dev.lions.unionflow.server.entity; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Versement") +class VersementTest { + + private static Membre newMembre() { + Membre m = new Membre(); + m.setId(UUID.randomUUID()); + m.setNumeroMembre("M1"); + m.setPrenom("A"); + m.setNom("B"); + m.setEmail("a@test.com"); + m.setDateNaissance(java.time.LocalDate.now()); + return m; + } + + @Test + @DisplayName("getters/setters") + void gettersSetters() { + Versement v = new Versement(); + v.setNumeroReference("VRS-2026-001"); + v.setMontant(new BigDecimal("10000.00")); + v.setCodeDevise("XOF"); + v.setMethodePaiement("WAVE"); + v.setStatutPaiement("CONFIRME"); + v.setDatePaiement(LocalDateTime.now()); + v.setNumeroTelephone("771234567"); + v.setMembre(newMembre()); + + assertThat(v.getNumeroReference()).isEqualTo("VRS-2026-001"); + assertThat(v.getMontant()).isEqualByComparingTo("10000.00"); + assertThat(v.getCodeDevise()).isEqualTo("XOF"); + assertThat(v.getStatutPaiement()).isEqualTo("CONFIRME"); + assertThat(v.getNumeroTelephone()).isEqualTo("771234567"); + } + + @Test + @DisplayName("genererNumeroReference commence par VRS-") + void genererNumeroReference_startsWithVRS() { + String ref = Versement.genererNumeroReference(); + assertThat(ref).startsWith("VRS-").isNotNull(); + } + + @Test + @DisplayName("genererNumeroReference retourne des références uniques") + void genererNumeroReference_unique() { + String ref1 = Versement.genererNumeroReference(); + String ref2 = Versement.genererNumeroReference(); + assertThat(ref1).isNotEqualTo(ref2); + } + + @Test + @DisplayName("isConfirme — statut CONFIRME") + void isConfirme_statutCONFIRME() { + Versement v = buildMinimal(); + v.setStatutPaiement("CONFIRME"); + assertThat(v.isConfirme()).isTrue(); + assertThat(v.peutEtreModifie()).isFalse(); + } + + @Test + @DisplayName("isConfirme — statut VALIDE (compatibilité)") + void isConfirme_statutVALIDE() { + Versement v = buildMinimal(); + v.setStatutPaiement("VALIDE"); + assertThat(v.isConfirme()).isTrue(); + assertThat(v.peutEtreModifie()).isFalse(); + } + + @Test + @DisplayName("isConfirme — statut EN_ATTENTE") + void isConfirme_statutEnAttente() { + Versement v = buildMinimal(); + v.setStatutPaiement("EN_ATTENTE"); + assertThat(v.isConfirme()).isFalse(); + assertThat(v.peutEtreModifie()).isTrue(); + } + + @Test + @DisplayName("peutEtreModifie — statut ANNULE") + void peutEtreModifie_false_whenANNULE() { + Versement v = buildMinimal(); + v.setStatutPaiement("ANNULE"); + assertThat(v.peutEtreModifie()).isFalse(); + } + + @Test + @DisplayName("peutEtreModifie — statut EN_ATTENTE_VALIDATION") + void peutEtreModifie_true_whenEnAttenteValidation() { + Versement v = buildMinimal(); + v.setStatutPaiement("EN_ATTENTE_VALIDATION"); + assertThat(v.peutEtreModifie()).isTrue(); + } + + @Test + @DisplayName("onCreate initialise reference, statut et datePaiement si null") + void onCreate_initDefaults() { + Versement v = new Versement(); + v.setMontant(BigDecimal.TEN); + v.setCodeDevise("XOF"); + v.setMethodePaiement("WAVE"); + v.setMembre(newMembre()); + + v.onCreate(); + + assertThat(v.getNumeroReference()).startsWith("VRS-"); + assertThat(v.getStatutPaiement()).isEqualTo("EN_ATTENTE"); + assertThat(v.getDatePaiement()).isNotNull(); + } + + @Test + @DisplayName("onCreate conserve la référence existante") + void onCreate_preservesExistingReference() { + Versement v = buildMinimal(); + v.setNumeroReference("VRS-CUSTOM-001"); + v.onCreate(); + assertThat(v.getNumeroReference()).isEqualTo("VRS-CUSTOM-001"); + } + + @Test + @DisplayName("equals et hashCode") + void equalsHashCode() { + UUID id = UUID.randomUUID(); + Membre m = newMembre(); + Versement a = buildMinimal(); + a.setId(id); + a.setMembre(m); + Versement b = buildMinimal(); + b.setId(id); + b.setMembre(m); + assertThat(a).isEqualTo(b); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + } + + @Test + @DisplayName("toString non null") + void toString_nonNull() { + assertThat(buildMinimal().toString()).isNotNull().isNotEmpty(); + } + + private Versement buildMinimal() { + Versement v = new Versement(); + v.setNumeroReference("VRS-2026-TEST"); + v.setMontant(BigDecimal.ONE); + v.setCodeDevise("XOF"); + v.setMethodePaiement("WAVE"); + v.setStatutPaiement("EN_ATTENTE"); + v.setMembre(newMembre()); + return v; + } +} diff --git a/src/test/java/dev/lions/unionflow/server/repository/ContactPolicyRepositoryTest.java b/src/test/java/dev/lions/unionflow/server/repository/ContactPolicyRepositoryTest.java new file mode 100644 index 0000000..610a5f9 --- /dev/null +++ b/src/test/java/dev/lions/unionflow/server/repository/ContactPolicyRepositoryTest.java @@ -0,0 +1,43 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.ContactPolicy; +import io.quarkus.test.TestTransaction; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@QuarkusTest +@DisplayName("ContactPolicyRepository") +class ContactPolicyRepositoryTest { + + @Inject + ContactPolicyRepository contactPolicyRepository; + + @Test + @TestTransaction + @DisplayName("findByOrganisationId retourne empty pour organisation inexistante") + void findByOrganisationId_inexistant_returnsEmpty() { + Optional opt = contactPolicyRepository.findByOrganisationId(UUID.randomUUID()); + assertThat(opt).isEmpty(); + } + + @Test + @TestTransaction + @DisplayName("listAll retourne liste non nulle") + void listAll_returnsNonNull() { + assertThat(contactPolicyRepository.listAll()).isNotNull(); + } + + @Test + @TestTransaction + @DisplayName("count retourne >= 0") + void count_returnsNonNegative() { + assertThat(contactPolicyRepository.count()).isGreaterThanOrEqualTo(0); + } +} diff --git a/src/test/java/dev/lions/unionflow/server/repository/ConversationParticipantRepositoryTest.java b/src/test/java/dev/lions/unionflow/server/repository/ConversationParticipantRepositoryTest.java new file mode 100644 index 0000000..b712fa5 --- /dev/null +++ b/src/test/java/dev/lions/unionflow/server/repository/ConversationParticipantRepositoryTest.java @@ -0,0 +1,63 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.ConversationParticipant; +import io.quarkus.test.TestTransaction; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@QuarkusTest +@DisplayName("ConversationParticipantRepository") +class ConversationParticipantRepositoryTest { + + @Inject + ConversationParticipantRepository conversationParticipantRepository; + + @Test + @TestTransaction + @DisplayName("findParticipant retourne empty pour conversation inexistante") + void findParticipant_returnsEmpty() { + Optional opt = + conversationParticipantRepository.findParticipant(UUID.randomUUID(), UUID.randomUUID()); + assertThat(opt).isEmpty(); + } + + @Test + @TestTransaction + @DisplayName("findByConversation retourne liste vide pour conversation inexistante") + void findByConversation_returnsEmpty() { + List list = + conversationParticipantRepository.findByConversation(UUID.randomUUID()); + assertThat(list).isNotNull().isEmpty(); + } + + @Test + @TestTransaction + @DisplayName("estParticipant retourne false pour conversation inexistante") + void estParticipant_returnsFalse() { + boolean result = conversationParticipantRepository.estParticipant( + UUID.randomUUID(), UUID.randomUUID()); + assertThat(result).isFalse(); + } + + @Test + @TestTransaction + @DisplayName("count retourne >= 0") + void count_returnsNonNegative() { + assertThat(conversationParticipantRepository.count()).isGreaterThanOrEqualTo(0); + } + + @Test + @TestTransaction + @DisplayName("listAll retourne liste non nulle") + void listAll_returnsNonNull() { + assertThat(conversationParticipantRepository.listAll()).isNotNull(); + } +} diff --git a/src/test/java/dev/lions/unionflow/server/repository/MemberBlockRepositoryTest.java b/src/test/java/dev/lions/unionflow/server/repository/MemberBlockRepositoryTest.java new file mode 100644 index 0000000..3ccfdb9 --- /dev/null +++ b/src/test/java/dev/lions/unionflow/server/repository/MemberBlockRepositoryTest.java @@ -0,0 +1,64 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.MemberBlock; +import io.quarkus.test.TestTransaction; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@QuarkusTest +@DisplayName("MemberBlockRepository") +class MemberBlockRepositoryTest { + + @Inject + MemberBlockRepository memberBlockRepository; + + @Test + @TestTransaction + @DisplayName("estBloque retourne false si aucun blocage") + void estBloque_returns_false() { + boolean result = memberBlockRepository.estBloque( + UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()); + assertThat(result).isFalse(); + } + + @Test + @TestTransaction + @DisplayName("findBlocage retourne empty si aucun blocage") + void findBlocage_returnsEmpty() { + Optional opt = memberBlockRepository.findBlocage( + UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()); + assertThat(opt).isEmpty(); + } + + @Test + @TestTransaction + @DisplayName("findByBloqueur retourne liste vide pour membre inexistant") + void findByBloqueur_returnsEmpty() { + List list = memberBlockRepository.findByBloqueur(UUID.randomUUID()); + assertThat(list).isNotNull().isEmpty(); + } + + @Test + @TestTransaction + @DisplayName("findByBloqueurEtOrganisation retourne liste vide") + void findByBloqueurEtOrganisation_returnsEmpty() { + List list = memberBlockRepository.findByBloqueurEtOrganisation( + UUID.randomUUID(), UUID.randomUUID()); + assertThat(list).isNotNull().isEmpty(); + } + + @Test + @TestTransaction + @DisplayName("count retourne >= 0") + void count_returnsNonNegative() { + assertThat(memberBlockRepository.count()).isGreaterThanOrEqualTo(0); + } +} diff --git a/src/test/java/dev/lions/unionflow/server/repository/VersementRepositoryTest.java b/src/test/java/dev/lions/unionflow/server/repository/VersementRepositoryTest.java new file mode 100644 index 0000000..763a7b3 --- /dev/null +++ b/src/test/java/dev/lions/unionflow/server/repository/VersementRepositoryTest.java @@ -0,0 +1,109 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.api.enums.paiement.MethodePaiement; +import dev.lions.unionflow.server.api.enums.paiement.StatutPaiement; +import dev.lions.unionflow.server.entity.Versement; +import io.quarkus.test.TestTransaction; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@QuarkusTest +@DisplayName("VersementRepository") +class VersementRepositoryTest { + + @Inject + VersementRepository versementRepository; + + @Test + @TestTransaction + @DisplayName("findById retourne null pour UUID inexistant") + void findById_inexistant_returnsNull() { + assertThat(versementRepository.findById(UUID.randomUUID())).isNull(); + } + + @Test + @TestTransaction + @DisplayName("findVersementById retourne empty pour UUID inexistant") + void findVersementById_inexistant_returnsEmpty() { + Optional opt = versementRepository.findVersementById(UUID.randomUUID()); + assertThat(opt).isEmpty(); + } + + @Test + @TestTransaction + @DisplayName("findByNumeroReference retourne empty pour référence inexistante") + void findByNumeroReference_inexistant_returnsEmpty() { + Optional opt = versementRepository.findByNumeroReference("VRS-" + UUID.randomUUID()); + assertThat(opt).isEmpty(); + } + + @Test + @TestTransaction + @DisplayName("listAll retourne une liste") + void listAll_returnsList() { + List list = versementRepository.listAll(); + assertThat(list).isNotNull(); + } + + @Test + @TestTransaction + @DisplayName("count retourne un nombre >= 0") + void count_returnsNonNegative() { + assertThat(versementRepository.count()).isGreaterThanOrEqualTo(0); + } + + @Test + @TestTransaction + @DisplayName("findByMembreId retourne liste vide pour membre inexistant") + void findByMembreId_inconnu_returnsEmpty() { + List list = versementRepository.findByMembreId(UUID.randomUUID()); + assertThat(list).isNotNull().isEmpty(); + } + + @Test + @TestTransaction + @DisplayName("findByStatut retourne liste non nulle") + void findByStatut_returnsNonNull() { + List list = versementRepository.findByStatut(StatutPaiement.EN_ATTENTE); + assertThat(list).isNotNull(); + } + + @Test + @TestTransaction + @DisplayName("findByMethode retourne liste non nulle") + void findByMethode_returnsNonNull() { + List list = versementRepository.findByMethode(MethodePaiement.WAVE_MOBILE_MONEY); + assertThat(list).isNotNull(); + } + + @Test + @TestTransaction + @DisplayName("findConfirmesParPeriode retourne liste non nulle") + void findConfirmesParPeriode_returnsNonNull() { + LocalDateTime debut = LocalDateTime.now().minusDays(30); + LocalDateTime fin = LocalDateTime.now(); + List list = versementRepository.findConfirmesParPeriode(debut, fin); + assertThat(list).isNotNull(); + } + + @Test + @TestTransaction + @DisplayName("calculerMontantTotalConfirmes retourne ZERO si aucun versement") + void calculerMontantTotalConfirmes_returnsZeroWhenEmpty() { + LocalDateTime debut = LocalDateTime.now().minusSeconds(1); + LocalDateTime fin = LocalDateTime.now().plusSeconds(1); + BigDecimal total = versementRepository.calculerMontantTotalConfirmes(debut, fin); + // Peut être > 0 selon les données de test, mais ne doit pas être null + assertThat(total).isNotNull().isGreaterThanOrEqualTo(BigDecimal.ZERO); + } +} diff --git a/src/test/java/dev/lions/unionflow/server/resource/MessagingResourceTest.java b/src/test/java/dev/lions/unionflow/server/resource/MessagingResourceTest.java new file mode 100644 index 0000000..6896625 --- /dev/null +++ b/src/test/java/dev/lions/unionflow/server/resource/MessagingResourceTest.java @@ -0,0 +1,439 @@ +package dev.lions.unionflow.server.resource; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +import dev.lions.unionflow.server.api.dto.messagerie.response.ContactPolicyResponse; +import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationResponse; +import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationSummaryResponse; +import dev.lions.unionflow.server.api.dto.messagerie.response.MessageResponse; +import dev.lions.unionflow.server.service.MessagingService; +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import jakarta.ws.rs.NotFoundException; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +/** + * Tests d'intégration REST pour {@link MessagingResource}. + * + * @author UnionFlow Team + * @version 4.0 + * @since 2026-04-13 + */ +@QuarkusTest +@DisplayName("MessagingResource") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MessagingResourceTest { + + private static final String BASE = "/api/messagerie"; + private static final String CONV_ID = "00000000-0000-0000-0000-000000000001"; + private static final String MSG_ID = "00000000-0000-0000-0000-000000000002"; + private static final String MEMBRE_ID = "00000000-0000-0000-0000-000000000003"; + private static final String ORG_ID = "00000000-0000-0000-0000-000000000004"; + + @InjectMock + MessagingService messagingService; + + // ── POST /conversations/directe ─────────────────────────────────────────── + + @Test + @Order(1) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("demarrerConversationDirecte — requête valide → 201") + void demarrerConversationDirecte_valid_returns201() { + ConversationResponse conv = ConversationResponse.builder() + .id(UUID.fromString(CONV_ID)) + .typeConversation("DIRECTE") + .statut("ACTIVE") + .build(); + when(messagingService.demarrerConversationDirecte(any())).thenReturn(conv); + + given() + .contentType(ContentType.JSON) + .body("{\"destinataireId\":\"" + MEMBRE_ID + "\",\"organisationId\":\"" + ORG_ID + "\"}") + .when().post(BASE + "/conversations/directe") + .then().statusCode(201) + .body("typeConversation", equalTo("DIRECTE")) + .body("statut", equalTo("ACTIVE")); + } + + @Test + @Order(2) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("demarrerConversationDirecte — destinataireId manquant → 400") + void demarrerConversationDirecte_missingDestinataire_returns400() { + given() + .contentType(ContentType.JSON) + .body("{\"organisationId\":\"" + ORG_ID + "\"}") + .when().post(BASE + "/conversations/directe") + .then().statusCode(400); + } + + // ── POST /conversations/role ────────────────────────────────────────────── + + @Test + @Order(3) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("demarrerConversationRole — requête valide → 201") + void demarrerConversationRole_valid_returns201() { + ConversationResponse conv = ConversationResponse.builder() + .id(UUID.fromString(CONV_ID)) + .typeConversation("ROLE_CANAL") + .roleCible("TRESORIER") + .build(); + when(messagingService.demarrerConversationRole(any())).thenReturn(conv); + + given() + .contentType(ContentType.JSON) + .body("{\"organisationId\":\"" + ORG_ID + "\"," + + "\"roleCible\":\"TRESORIER\"," + + "\"contenuInitial\":\"Bonjour\"}") + .when().post(BASE + "/conversations/role") + .then().statusCode(201) + .body("typeConversation", equalTo("ROLE_CANAL")) + .body("roleCible", equalTo("TRESORIER")); + } + + @Test + @Order(4) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("demarrerConversationRole — rôle invalide → 400") + void demarrerConversationRole_invalidRole_returns400() { + given() + .contentType(ContentType.JSON) + .body("{\"organisationId\":\"" + ORG_ID + "\"," + + "\"roleCible\":\"DIRECTEUR\"," + + "\"contenuInitial\":\"Bonjour\"}") + .when().post(BASE + "/conversations/role") + .then().statusCode(400); + } + + // ── GET /conversations ──────────────────────────────────────────────────── + + @Test + @Order(5) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("getMesConversations — liste vide → 200") + void getMesConversations_empty_returns200() { + when(messagingService.getMesConversations()).thenReturn(Collections.emptyList()); + + given() + .when().get(BASE + "/conversations") + .then().statusCode(200); + } + + @Test + @Order(6) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("getMesConversations — liste non vide → 200") + void getMesConversations_nonEmpty_returns200() { + ConversationSummaryResponse summary = ConversationSummaryResponse.builder() + .id(UUID.fromString(CONV_ID)) + .typeConversation("DIRECTE") + .titre("Alice Dupont") + .build(); + when(messagingService.getMesConversations()).thenReturn(List.of(summary)); + + given() + .when().get(BASE + "/conversations") + .then().statusCode(200) + .body("[0].titre", equalTo("Alice Dupont")); + } + + // ── GET /conversations/{id} ─────────────────────────────────────────────── + + @Test + @Order(7) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("getConversation — trouvée → 200") + void getConversation_found_returns200() { + ConversationResponse conv = ConversationResponse.builder() + .id(UUID.fromString(CONV_ID)) + .typeConversation("DIRECTE") + .titre("Bob Martin") + .build(); + when(messagingService.getConversation(any(UUID.class))).thenReturn(conv); + + given() + .when().get(BASE + "/conversations/" + CONV_ID) + .then().statusCode(200) + .body("titre", equalTo("Bob Martin")); + } + + @Test + @Order(8) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("getConversation — non trouvée → 404") + void getConversation_notFound_returns404() { + when(messagingService.getConversation(any(UUID.class))) + .thenThrow(new NotFoundException("conversation non trouvée")); + + given() + .when().get(BASE + "/conversations/" + CONV_ID) + .then().statusCode(404); + } + + // ── DELETE /conversations/{id} ──────────────────────────────────────────── + + @Test + @Order(9) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("archiverConversation → 200") + void archiverConversation_returns200() { + ConversationResponse conv = ConversationResponse.builder() + .id(UUID.fromString(CONV_ID)) + .statut("ARCHIVEE") + .build(); + when(messagingService.archiverConversation(any(UUID.class))).thenReturn(conv); + + given() + .when().delete(BASE + "/conversations/" + CONV_ID) + .then().statusCode(200) + .body("statut", equalTo("ARCHIVEE")); + } + + // ── POST /conversations/{id}/messages ───────────────────────────────────── + + @Test + @Order(10) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("envoyerMessage — message texte → 201") + void envoyerMessage_texte_returns201() { + MessageResponse msg = MessageResponse.builder() + .id(UUID.fromString(MSG_ID)) + .typeMessage("TEXTE") + .contenu("Bonjour !") + .build(); + when(messagingService.envoyerMessage(any(UUID.class), any())).thenReturn(msg); + + given() + .contentType(ContentType.JSON) + .body("{\"typeMessage\":\"TEXTE\",\"contenu\":\"Bonjour !\"}") + .when().post(BASE + "/conversations/" + CONV_ID + "/messages") + .then().statusCode(201) + .body("typeMessage", equalTo("TEXTE")) + .body("contenu", equalTo("Bonjour !")); + } + + @Test + @Order(11) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("envoyerMessage — type invalide → 400") + void envoyerMessage_invalidType_returns400() { + given() + .contentType(ContentType.JSON) + .body("{\"typeMessage\":\"VIDEO\",\"contenu\":\"test\"}") + .when().post(BASE + "/conversations/" + CONV_ID + "/messages") + .then().statusCode(400); + } + + // ── GET /conversations/{id}/messages ────────────────────────────────────── + + @Test + @Order(12) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("getMessages — page 0 → 200") + void getMessages_page0_returns200() { + when(messagingService.getMessages(any(UUID.class), anyInt())) + .thenReturn(Collections.emptyList()); + + given() + .when().get(BASE + "/conversations/" + CONV_ID + "/messages") + .then().statusCode(200); + } + + @Test + @Order(13) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("getMessages — page custom → 200") + void getMessages_customPage_returns200() { + when(messagingService.getMessages(any(UUID.class), anyInt())) + .thenReturn(Collections.emptyList()); + + given() + .queryParam("page", 2) + .when().get(BASE + "/conversations/" + CONV_ID + "/messages") + .then().statusCode(200); + } + + // ── PUT /conversations/{id}/lire ────────────────────────────────────────── + + @Test + @Order(14) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("marquerLu → 204") + void marquerLu_returns204() { + doNothing().when(messagingService).marquerConversationLue(any(UUID.class)); + + given() + .when().put(BASE + "/conversations/" + CONV_ID + "/lire") + .then().statusCode(204); + } + + // ── DELETE /conversations/{cId}/messages/{mId} ──────────────────────────── + + @Test + @Order(15) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("supprimerMessage → 204") + void supprimerMessage_returns204() { + doNothing().when(messagingService).supprimerMessage(any(UUID.class), any(UUID.class)); + + given() + .when().delete(BASE + "/conversations/" + CONV_ID + "/messages/" + MSG_ID) + .then().statusCode(204); + } + + @Test + @Order(16) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("supprimerMessage — non trouvé → 404") + void supprimerMessage_notFound_returns404() { + org.mockito.Mockito.doThrow(new NotFoundException("message introuvable")) + .when(messagingService).supprimerMessage(any(UUID.class), any(UUID.class)); + + given() + .when().delete(BASE + "/conversations/" + CONV_ID + "/messages/" + MSG_ID) + .then().statusCode(404); + } + + // ── POST /blocages ──────────────────────────────────────────────────────── + + @Test + @Order(17) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("bloquerMembre — requête valide → 204") + void bloquerMembre_valid_returns204() { + doNothing().when(messagingService).bloquerMembre(any()); + + given() + .contentType(ContentType.JSON) + .body("{\"membreABloquerId\":\"" + MEMBRE_ID + "\"," + + "\"organisationId\":\"" + ORG_ID + "\"}") + .when().post(BASE + "/blocages") + .then().statusCode(204); + } + + @Test + @Order(18) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("bloquerMembre — membreABloquerId manquant → 400") + void bloquerMembre_missingId_returns400() { + given() + .contentType(ContentType.JSON) + .body("{\"organisationId\":\"" + ORG_ID + "\"}") + .when().post(BASE + "/blocages") + .then().statusCode(400); + } + + // ── DELETE /blocages/{membreId} ─────────────────────────────────────────── + + @Test + @Order(19) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("debloquerMembre → 204") + void debloquerMembre_returns204() { + doNothing().when(messagingService).debloquerMembre(any(UUID.class), any()); + + given() + .queryParam("organisationId", ORG_ID) + .when().delete(BASE + "/blocages/" + MEMBRE_ID) + .then().statusCode(204); + } + + // ── GET /blocages ───────────────────────────────────────────────────────── + + @Test + @Order(20) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("getMesBlocages → 200") + void getMesBlocages_returns200() { + when(messagingService.getMesBlocages()).thenReturn(Collections.emptyList()); + + given() + .when().get(BASE + "/blocages") + .then().statusCode(200); + } + + // ── GET /politique/{organisationId} ─────────────────────────────────────── + + @Test + @Order(21) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("getPolitique → 200") + void getPolitique_returns200() { + ContactPolicyResponse policy = ContactPolicyResponse.builder() + .id(UUID.randomUUID()) + .organisationId(UUID.fromString(ORG_ID)) + .typePolitique("OUVERT") + .autoriserMembreVersMembre(true) + .autoriserMembreVersRole(true) + .autoriserNotesVocales(true) + .build(); + when(messagingService.getPolitique(any(UUID.class))).thenReturn(policy); + + given() + .when().get(BASE + "/politique/" + ORG_ID) + .then().statusCode(200) + .body("typePolitique", equalTo("OUVERT")) + .body("autoriserNotesVocales", equalTo(true)); + } + + // ── PUT /politique/{organisationId} ─────────────────────────────────────── + + @Test + @Order(22) + @TestSecurity(user = "admin@test.com", roles = {"ADMIN"}) + @DisplayName("mettreAJourPolitique — ADMIN → 200") + void mettreAJourPolitique_admin_returns200() { + ContactPolicyResponse updated = ContactPolicyResponse.builder() + .typePolitique("BUREAU_SEULEMENT") + .build(); + when(messagingService.mettreAJourPolitique(any(UUID.class), any())).thenReturn(updated); + + given() + .contentType(ContentType.JSON) + .body("{\"typePolitique\":\"BUREAU_SEULEMENT\"}") + .when().put(BASE + "/politique/" + ORG_ID) + .then().statusCode(200) + .body("typePolitique", equalTo("BUREAU_SEULEMENT")); + } + + @Test + @Order(23) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + @DisplayName("mettreAJourPolitique — MEMBRE → 403") + void mettreAJourPolitique_membre_returns403() { + given() + .contentType(ContentType.JSON) + .body("{\"typePolitique\":\"BUREAU_SEULEMENT\"}") + .when().put(BASE + "/politique/" + ORG_ID) + .then().statusCode(403); + } + + @Test + @Order(24) + @TestSecurity(user = "admin@test.com", roles = {"ADMIN"}) + @DisplayName("mettreAJourPolitique — politique invalide → 400") + void mettreAJourPolitique_invalidPolicy_returns400() { + given() + .contentType(ContentType.JSON) + .body("{\"typePolitique\":\"LIBRE\"}") + .when().put(BASE + "/politique/" + ORG_ID) + .then().statusCode(400); + } +} diff --git a/src/test/java/dev/lions/unionflow/server/resource/VersementResourceTest.java b/src/test/java/dev/lions/unionflow/server/resource/VersementResourceTest.java new file mode 100644 index 0000000..735d24c --- /dev/null +++ b/src/test/java/dev/lions/unionflow/server/resource/VersementResourceTest.java @@ -0,0 +1,317 @@ +package dev.lions.unionflow.server.resource; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +import dev.lions.unionflow.server.api.dto.versement.response.VersementGatewayResponse; +import dev.lions.unionflow.server.api.dto.versement.response.VersementResponse; +import dev.lions.unionflow.server.api.dto.versement.response.VersementStatutResponse; +import dev.lions.unionflow.server.api.dto.versement.response.VersementSummaryResponse; +import dev.lions.unionflow.server.service.VersementService; +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import jakarta.ws.rs.NotFoundException; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +/** + * Tests d'intégration REST pour {@link VersementResource}. + * + * @author UnionFlow Team + * @version 4.0 + * @since 2026-04-13 + */ +@QuarkusTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class VersementResourceTest { + + private static final String BASE = "/api/versements"; + private static final String VERSEMENT_ID = "00000000-0000-0000-0000-000000000010"; + private static final String MEMBRE_ID = "00000000-0000-0000-0000-000000000020"; + private static final String INTENTION_ID = "00000000-0000-0000-0000-000000000030"; + + @InjectMock + VersementService versementService; + + // ── GET /{id} ───────────────────────────────────────────────────────────── + + @Test + @Order(1) + @TestSecurity(user = "admin@test.com", roles = {"ADMIN"}) + void trouverParId_found_returns200() { + VersementResponse response = VersementResponse.builder() + .numeroReference("VRS-2026-001") + .build(); + when(versementService.trouverParId(any(UUID.class))).thenReturn(response); + + given() + .when().get(BASE + "/" + VERSEMENT_ID) + .then().statusCode(200) + .body("numeroReference", equalTo("VRS-2026-001")); + } + + @Test + @Order(2) + @TestSecurity(user = "admin@test.com", roles = {"ADMIN"}) + void trouverParId_notFound_returns404() { + when(versementService.trouverParId(any(UUID.class))) + .thenThrow(new NotFoundException("non trouvé")); + + given() + .when().get(BASE + "/" + VERSEMENT_ID) + .then().statusCode(404); + } + + // ── GET /reference/{ref} ────────────────────────────────────────────────── + + @Test + @Order(3) + @TestSecurity(user = "admin@test.com", roles = {"ADMIN"}) + void trouverParReference_found_returns200() { + VersementResponse response = VersementResponse.builder() + .numeroReference("VRS-2026-001") + .build(); + when(versementService.trouverParNumeroReference(anyString())).thenReturn(response); + + given() + .when().get(BASE + "/reference/VRS-2026-001") + .then().statusCode(200); + } + + // ── GET /membre/{id} ────────────────────────────────────────────────────── + + @Test + @Order(4) + @TestSecurity(user = "admin@test.com", roles = {"ADMIN"}) + void listerParMembre_returns200() { + when(versementService.listerParMembre(any(UUID.class))) + .thenReturn(List.of(new VersementSummaryResponse())); + + given() + .when().get(BASE + "/membre/" + MEMBRE_ID) + .then().statusCode(200); + } + + // ── GET /mes-versements ──────────────────────────────────────────────────── + + @Test + @Order(5) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void getMesVersements_returns200() { + when(versementService.getMesVersements(anyInt())) + .thenReturn(Collections.emptyList()); + + given() + .when().get(BASE + "/mes-versements") + .then().statusCode(200); + } + + @Test + @Order(6) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void getMesVersements_customLimit_returns200() { + when(versementService.getMesVersements(10)) + .thenReturn(Collections.emptyList()); + + given() + .queryParam("limit", 10) + .when().get(BASE + "/mes-versements") + .then().statusCode(200); + } + + // ── POST /{id}/valider ──────────────────────────────────────────────────── + + @Test + @Order(7) + @TestSecurity(user = "admin@test.com", roles = {"ADMIN"}) + void validerVersement_returns200() { + VersementResponse response = VersementResponse.builder() + .statutPaiement("CONFIRME") + .build(); + when(versementService.validerVersement(any(UUID.class))).thenReturn(response); + + given() + .contentType(ContentType.JSON) + .when().post(BASE + "/" + VERSEMENT_ID + "/valider") + .then().statusCode(200) + .body("statutPaiement", equalTo("CONFIRME")); + } + + @Test + @Order(8) + @TestSecurity(user = "admin@test.com", roles = {"ADMIN"}) + void validerVersement_notFound_returns404() { + when(versementService.validerVersement(any(UUID.class))) + .thenThrow(new NotFoundException("non trouvé")); + + given() + .contentType(ContentType.JSON) + .when().post(BASE + "/" + VERSEMENT_ID + "/valider") + .then().statusCode(404); + } + + // ── POST /{id}/annuler ──────────────────────────────────────────────────── + + @Test + @Order(9) + @TestSecurity(user = "admin@test.com", roles = {"ADMIN"}) + void annulerVersement_returns200() { + VersementResponse response = VersementResponse.builder() + .statutPaiement("ANNULE") + .build(); + when(versementService.annulerVersement(any(UUID.class))).thenReturn(response); + + given() + .contentType(ContentType.JSON) + .when().post(BASE + "/" + VERSEMENT_ID + "/annuler") + .then().statusCode(200); + } + + // ── POST /initier-wave ──────────────────────────────────────────────────── + + @Test + @Order(10) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void initierWave_validRequest_returns201() { + VersementGatewayResponse gateway = VersementGatewayResponse.builder() + .versementId(UUID.fromString(VERSEMENT_ID)) + .waveLaunchUrl("wave://checkout/abc123") + .waveCheckoutSessionId("cos-abc") + .clientReference(INTENTION_ID) + .montant(new BigDecimal("5000")) + .statut("EN_ATTENTE") + .build(); + when(versementService.initierVersementWave(any())).thenReturn(gateway); + + given() + .contentType(ContentType.JSON) + .body("{\"cotisationId\":\"" + UUID.randomUUID() + "\",\"numeroTelephone\":\"771234567\"}") + .when().post(BASE + "/initier-wave") + .then().statusCode(201) + .body("waveLaunchUrl", equalTo("wave://checkout/abc123")) + .body("waveCheckoutSessionId", equalTo("cos-abc")); + } + + @Test + @Order(11) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void initierWave_missingCotisationId_returns400() { + given() + .contentType(ContentType.JSON) + .body("{\"numeroTelephone\":\"771234567\"}") + .when().post(BASE + "/initier-wave") + .then().statusCode(400); + } + + // ── GET /statut/{intentionId} ───────────────────────────────────────────── + + @Test + @Order(12) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void getStatutVersement_completee_returns200() { + VersementStatutResponse statut = VersementStatutResponse.builder() + .intentionId(UUID.fromString(INTENTION_ID)) + .statut("COMPLETEE") + .confirme(true) + .message("Versement confirmé !") + .build(); + when(versementService.verifierStatutVersement(any(UUID.class))).thenReturn(statut); + + given() + .when().get(BASE + "/statut/" + INTENTION_ID) + .then().statusCode(200) + .body("confirme", equalTo(true)) + .body("statut", equalTo("COMPLETEE")); + } + + @Test + @Order(13) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void getStatutVersement_notFound_returns404() { + when(versementService.verifierStatutVersement(any(UUID.class))) + .thenThrow(new NotFoundException("non trouvé")); + + given() + .when().get(BASE + "/statut/" + INTENTION_ID) + .then().statusCode(404); + } + + // ── POST /declarer-manuel ───────────────────────────────────────────────── + + @Test + @Order(14) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void declarerManuel_validRequest_returns201() { + VersementResponse response = VersementResponse.builder() + .statutPaiement("EN_ATTENTE_VALIDATION") + .methodePaiement("ESPECES") + .build(); + when(versementService.declarerVersementManuel(any())).thenReturn(response); + + given() + .contentType(ContentType.JSON) + .body("{\"cotisationId\":\"" + UUID.randomUUID() + + "\",\"methodePaiement\":\"ESPECES\"}") + .when().post(BASE + "/declarer-manuel") + .then().statusCode(201) + .body("statutPaiement", equalTo("EN_ATTENTE_VALIDATION")); + } + + @Test + @Order(15) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void declarerManuel_methodInvalide_returns400() { + given() + .contentType(ContentType.JSON) + .body("{\"cotisationId\":\"" + UUID.randomUUID() + + "\",\"methodePaiement\":\"PAYPAL\"}") + .when().post(BASE + "/declarer-manuel") + .then().statusCode(400); + } + + // ── POST /initier-depot-epargne ─────────────────────────────────────────── + + @Test + @Order(16) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void initierDepotEpargne_validRequest_returns201() { + VersementGatewayResponse gateway = VersementGatewayResponse.builder() + .waveLaunchUrl("wave://checkout/epargne") + .statut("EN_ATTENTE") + .montant(new BigDecimal("10000")) + .build(); + when(versementService.initierDepotEpargneEnLigne(any())).thenReturn(gateway); + + given() + .contentType(ContentType.JSON) + .body("{\"compteId\":\"" + UUID.randomUUID() + + "\",\"montant\":10000,\"numeroTelephone\":\"771234567\"}") + .when().post(BASE + "/initier-depot-epargne") + .then().statusCode(201) + .body("waveLaunchUrl", notNullValue()); + } + + @Test + @Order(17) + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void initierDepotEpargne_montantManquant_returns400() { + given() + .contentType(ContentType.JSON) + .body("{\"compteId\":\"" + UUID.randomUUID() + "\"}") + .when().post(BASE + "/initier-depot-epargne") + .then().statusCode(400); + } +} diff --git a/src/test/java/dev/lions/unionflow/server/service/MessagingServiceTest.java b/src/test/java/dev/lions/unionflow/server/service/MessagingServiceTest.java new file mode 100644 index 0000000..cab01d5 --- /dev/null +++ b/src/test/java/dev/lions/unionflow/server/service/MessagingServiceTest.java @@ -0,0 +1,509 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.messagerie.request.BloquerMembreRequest; +import dev.lions.unionflow.server.api.dto.messagerie.request.DemarrerConversationDirecteRequest; +import dev.lions.unionflow.server.api.dto.messagerie.request.DemarrerConversationRoleRequest; +import dev.lions.unionflow.server.api.dto.messagerie.request.EnvoyerMessageRequest; +import dev.lions.unionflow.server.api.dto.messagerie.request.MettreAJourPolitiqueRequest; +import dev.lions.unionflow.server.api.dto.messagerie.response.ContactPolicyResponse; +import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationResponse; +import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationSummaryResponse; +import dev.lions.unionflow.server.api.dto.messagerie.response.MessageResponse; +import dev.lions.unionflow.server.api.enums.messagerie.StatutConversation; +import dev.lions.unionflow.server.api.enums.messagerie.TypeContenu; +import dev.lions.unionflow.server.api.enums.messagerie.TypeConversation; +import dev.lions.unionflow.server.api.enums.messagerie.TypePolitiqueCommunication; +import dev.lions.unionflow.server.entity.ContactPolicy; +import dev.lions.unionflow.server.entity.Conversation; +import dev.lions.unionflow.server.entity.ConversationParticipant; +import dev.lions.unionflow.server.entity.MemberBlock; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.entity.Message; +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.repository.ContactPolicyRepository; +import dev.lions.unionflow.server.repository.ConversationParticipantRepository; +import dev.lions.unionflow.server.repository.ConversationRepository; +import dev.lions.unionflow.server.repository.MemberBlockRepository; +import dev.lions.unionflow.server.repository.MembreOrganisationRepository; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.MessageRepository; +import dev.lions.unionflow.server.messaging.KafkaEventProducer; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.ForbiddenException; +import jakarta.ws.rs.NotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +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.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests unitaires du MessagingService avec mocks. + */ +@QuarkusTest +@DisplayName("MessagingService") +class MessagingServiceTest { + + @Inject + MessagingService messagingService; + + @InjectMock ConversationRepository conversationRepository; + @InjectMock ConversationParticipantRepository participantRepository; + @InjectMock MessageRepository messageRepository; + @InjectMock ContactPolicyRepository contactPolicyRepository; + @InjectMock MemberBlockRepository memberBlockRepository; + @InjectMock MembreRepository membreRepository; + @InjectMock MembreOrganisationRepository membreOrganisationRepository; + @InjectMock KafkaEventProducer kafkaEventProducer; + @InjectMock SecurityIdentity securityIdentity; + + private static final UUID MEMBRE_ID = UUID.fromString("00000000-0000-0000-0000-000000000001"); + private static final UUID DESTINATAIRE_ID = UUID.fromString("00000000-0000-0000-0000-000000000002"); + private static final UUID ORG_ID = UUID.fromString("00000000-0000-0000-0000-000000000003"); + private static final UUID CONV_ID = UUID.fromString("00000000-0000-0000-0000-000000000004"); + private static final UUID MESSAGE_ID = UUID.fromString("00000000-0000-0000-0000-000000000005"); + + @BeforeEach + void setUp() { + // Mock du membre connecté + io.quarkus.security.runtime.QuarkusPrincipal principal = + new io.quarkus.security.runtime.QuarkusPrincipal("membre@test.com"); + when(securityIdentity.getPrincipal()).thenReturn(principal); + + Membre moi = newMembre(MEMBRE_ID, "membre@test.com", "Alpha", "Diallo"); + when(membreRepository.find("email", "membre@test.com")) + .thenReturn(io.quarkus.hibernate.orm.panache.PanacheQuery.class.cast( + mockSingleResultQuery(moi))); + } + + // ── getMesConversations ─────────────────────────────────────────────────── + + @Test + @DisplayName("getMesConversations — retourne liste vide") + void getMesConversations_returnsEmpty() { + when(conversationRepository.findByMembreId(MEMBRE_ID)) + .thenReturn(Collections.emptyList()); + + List result = messagingService.getMesConversations(); + + assertThat(result).isNotNull().isEmpty(); + } + + @Test + @DisplayName("getMesConversations — retourne la liste des conversations") + void getMesConversations_returnsList() { + Conversation conv = buildConversation(TypeConversation.DIRECTE); + when(conversationRepository.findByMembreId(MEMBRE_ID)).thenReturn(List.of(conv)); + when(messageRepository.findDernierMessage(CONV_ID)).thenReturn(Optional.empty()); + when(messageRepository.countNonLus(CONV_ID, MEMBRE_ID)).thenReturn(0L); + when(participantRepository.findByConversation(CONV_ID)).thenReturn(Collections.emptyList()); + + List result = messagingService.getMesConversations(); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getTypeConversation()).isEqualTo("DIRECTE"); + } + + // ── getConversation ─────────────────────────────────────────────────────── + + @Test + @DisplayName("getConversation — notFound → NotFoundException") + void getConversation_notFound() { + when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> messagingService.getConversation(CONV_ID)) + .isInstanceOf(NotFoundException.class); + } + + @Test + @DisplayName("getConversation — non participant → ForbiddenException") + void getConversation_nonParticipant() { + Conversation conv = buildConversation(TypeConversation.DIRECTE); + when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv)); + when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(false); + + assertThatThrownBy(() -> messagingService.getConversation(CONV_ID)) + .isInstanceOf(ForbiddenException.class); + } + + @Test + @DisplayName("getConversation — success → retourne ConversationResponse") + void getConversation_success() { + Conversation conv = buildConversation(TypeConversation.DIRECTE); + when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv)); + when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true); + when(participantRepository.findByConversation(CONV_ID)).thenReturn(Collections.emptyList()); + when(messageRepository.findByConversationPagine(eq(CONV_ID), eq(0), anyInt())) + .thenReturn(Collections.emptyList()); + when(messageRepository.countNonLus(CONV_ID, MEMBRE_ID)).thenReturn(2L); + + ConversationResponse result = messagingService.getConversation(CONV_ID); + + assertThat(result).isNotNull(); + assertThat(result.getId()).isEqualTo(CONV_ID); + assertThat(result.getNonLus()).isEqualTo(2L); + } + + // ── archiverConversation ────────────────────────────────────────────────── + + @Test + @DisplayName("archiverConversation — success → statut ARCHIVEE") + void archiverConversation_success() { + Conversation conv = buildConversation(TypeConversation.DIRECTE); + when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv)); + when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true); + when(participantRepository.findByConversation(CONV_ID)).thenReturn(Collections.emptyList()); + when(messageRepository.findByConversationPagine(eq(CONV_ID), eq(0), anyInt())) + .thenReturn(Collections.emptyList()); + when(messageRepository.countNonLus(CONV_ID, MEMBRE_ID)).thenReturn(0L); + + ConversationResponse result = messagingService.archiverConversation(CONV_ID); + + assertThat(result.getStatut()).isEqualTo("ARCHIVEE"); + } + + // ── envoyerMessage ──────────────────────────────────────────────────────── + + @Test + @DisplayName("envoyerMessage — conversation non trouvée → NotFoundException") + void envoyerMessage_notFound() { + when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.empty()); + + EnvoyerMessageRequest req = EnvoyerMessageRequest.builder() + .typeMessage("TEXTE").contenu("Bonjour").build(); + + assertThatThrownBy(() -> messagingService.envoyerMessage(CONV_ID, req)) + .isInstanceOf(NotFoundException.class); + } + + @Test + @DisplayName("envoyerMessage — non participant → ForbiddenException") + void envoyerMessage_nonParticipant() { + Conversation conv = buildConversation(TypeConversation.DIRECTE); + when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv)); + when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(false); + + EnvoyerMessageRequest req = EnvoyerMessageRequest.builder() + .typeMessage("TEXTE").contenu("Bonjour").build(); + + assertThatThrownBy(() -> messagingService.envoyerMessage(CONV_ID, req)) + .isInstanceOf(ForbiddenException.class); + } + + @Test + @DisplayName("envoyerMessage — conversation archivée → BadRequestException") + void envoyerMessage_archived() { + Conversation conv = buildConversation(TypeConversation.DIRECTE); + conv.setStatut(StatutConversation.ARCHIVEE); + when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv)); + when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true); + + EnvoyerMessageRequest req = EnvoyerMessageRequest.builder() + .typeMessage("TEXTE").contenu("Bonjour").build(); + + assertThatThrownBy(() -> messagingService.envoyerMessage(CONV_ID, req)) + .isInstanceOf(BadRequestException.class); + } + + @Test + @DisplayName("envoyerMessage — TEXTE sans contenu → BadRequestException") + void envoyerMessage_texteSansContenu() { + Conversation conv = buildConversation(TypeConversation.DIRECTE); + when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv)); + when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true); + + EnvoyerMessageRequest req = EnvoyerMessageRequest.builder() + .typeMessage("TEXTE").contenu("").build(); + + assertThatThrownBy(() -> messagingService.envoyerMessage(CONV_ID, req)) + .isInstanceOf(BadRequestException.class); + } + + @Test + @DisplayName("envoyerMessage — VOCAL sans urlFichier → BadRequestException") + void envoyerMessage_vocalSansUrl() { + Conversation conv = buildConversation(TypeConversation.DIRECTE); + when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv)); + when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true); + + EnvoyerMessageRequest req = EnvoyerMessageRequest.builder() + .typeMessage("VOCAL").dureeAudio(30).build(); + + assertThatThrownBy(() -> messagingService.envoyerMessage(CONV_ID, req)) + .isInstanceOf(BadRequestException.class); + } + + @Test + @DisplayName("envoyerMessage — VOCAL sans dureeAudio → BadRequestException") + void envoyerMessage_vocalSansDuree() { + Conversation conv = buildConversation(TypeConversation.DIRECTE); + when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv)); + when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true); + + EnvoyerMessageRequest req = EnvoyerMessageRequest.builder() + .typeMessage("VOCAL").urlFichier("https://example.com/audio.opus").build(); + + assertThatThrownBy(() -> messagingService.envoyerMessage(CONV_ID, req)) + .isInstanceOf(BadRequestException.class); + } + + // ── getMessages ─────────────────────────────────────────────────────────── + + @Test + @DisplayName("getMessages — success → retourne liste de messages") + void getMessages_success() { + Conversation conv = buildConversation(TypeConversation.DIRECTE); + Message msg = buildMessage(conv); + when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv)); + when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true); + when(messageRepository.findByConversationPagine(eq(CONV_ID), eq(0), anyInt())) + .thenReturn(List.of(msg)); + + List result = messagingService.getMessages(CONV_ID, 0); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getTypeMessage()).isEqualTo("TEXTE"); + } + + // ── marquerConversationLue ──────────────────────────────────────────────── + + @Test + @DisplayName("marquerConversationLue — participant trouvé → luJusqua mis à jour") + void marquerConversationLue_success() { + ConversationParticipant participant = new ConversationParticipant(); + participant.setMembre(newMembre(MEMBRE_ID, "membre@test.com", "Alpha", "Diallo")); + when(participantRepository.findParticipant(CONV_ID, MEMBRE_ID)) + .thenReturn(Optional.of(participant)); + + messagingService.marquerConversationLue(CONV_ID); + + assertThat(participant.getLuJusqua()).isNotNull(); + } + + @Test + @DisplayName("marquerConversationLue — participant absent → no-op") + void marquerConversationLue_noParticipant() { + when(participantRepository.findParticipant(CONV_ID, MEMBRE_ID)) + .thenReturn(Optional.empty()); + + // ne lève pas d'exception + messagingService.marquerConversationLue(CONV_ID); + } + + // ── supprimerMessage ────────────────────────────────────────────────────── + + @Test + @DisplayName("supprimerMessage — message non trouvé → NotFoundException") + void supprimerMessage_notFound() { + when(messageRepository.findMessageById(MESSAGE_ID)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> messagingService.supprimerMessage(CONV_ID, MESSAGE_ID)) + .isInstanceOf(NotFoundException.class); + } + + @Test + @DisplayName("supprimerMessage — message dans autre conversation → NotFoundException") + void supprimerMessage_wrongConversation() { + Conversation autreConv = buildConversation(TypeConversation.DIRECTE); + autreConv.setId(UUID.randomUUID()); // ID différent de CONV_ID + Message msg = buildMessage(autreConv); + when(messageRepository.findMessageById(MESSAGE_ID)).thenReturn(Optional.of(msg)); + + assertThatThrownBy(() -> messagingService.supprimerMessage(CONV_ID, MESSAGE_ID)) + .isInstanceOf(NotFoundException.class); + } + + @Test + @DisplayName("supprimerMessage — pas l'auteur → ForbiddenException") + void supprimerMessage_notAuthor() { + Conversation conv = buildConversation(TypeConversation.DIRECTE); + Membre autreExpedireur = newMembre(DESTINATAIRE_ID, "other@test.com", "Beta", "Koné"); + Message msg = new Message(); + msg.setId(MESSAGE_ID); + msg.setConversation(conv); + msg.setExpediteur(autreExpedireur); + msg.setTypeMessage(TypeContenu.TEXTE); + msg.setContenu("Message d'un autre"); + when(messageRepository.findMessageById(MESSAGE_ID)).thenReturn(Optional.of(msg)); + + assertThatThrownBy(() -> messagingService.supprimerMessage(CONV_ID, MESSAGE_ID)) + .isInstanceOf(ForbiddenException.class); + } + + // ── bloquerMembre ───────────────────────────────────────────────────────── + + @Test + @DisplayName("bloquerMembre — soi-même → BadRequestException") + void bloquerMembre_soiMeme() { + BloquerMembreRequest req = BloquerMembreRequest.builder() + .membreABloquerId(MEMBRE_ID) + .organisationId(ORG_ID) + .build(); + + assertThatThrownBy(() -> messagingService.bloquerMembre(req)) + .isInstanceOf(BadRequestException.class); + } + + @Test + @DisplayName("bloquerMembre — membre introuvable → NotFoundException") + void bloquerMembre_destinataireInconnu() { + when(membreRepository.findById(DESTINATAIRE_ID)).thenReturn(null); + + BloquerMembreRequest req = BloquerMembreRequest.builder() + .membreABloquerId(DESTINATAIRE_ID) + .organisationId(ORG_ID) + .build(); + + assertThatThrownBy(() -> messagingService.bloquerMembre(req)) + .isInstanceOf(NotFoundException.class); + } + + @Test + @DisplayName("bloquerMembre — déjà bloqué → BadRequestException") + void bloquerMembre_dejaBloque() { + Membre aBloquer = newMembre(DESTINATAIRE_ID, "dest@test.com", "Beta", "Koné"); + when(membreRepository.findById(DESTINATAIRE_ID)).thenReturn(aBloquer); + when(memberBlockRepository.estBloque(MEMBRE_ID, DESTINATAIRE_ID, ORG_ID)).thenReturn(true); + + BloquerMembreRequest req = BloquerMembreRequest.builder() + .membreABloquerId(DESTINATAIRE_ID) + .organisationId(ORG_ID) + .build(); + + assertThatThrownBy(() -> messagingService.bloquerMembre(req)) + .isInstanceOf(BadRequestException.class); + } + + // ── debloquerMembre ─────────────────────────────────────────────────────── + + @Test + @DisplayName("debloquerMembre — blocage non trouvé → NotFoundException") + void debloquerMembre_notFound() { + when(memberBlockRepository.findBlocage(MEMBRE_ID, DESTINATAIRE_ID, ORG_ID)) + .thenReturn(Optional.empty()); + + assertThatThrownBy(() -> messagingService.debloquerMembre(DESTINATAIRE_ID, ORG_ID)) + .isInstanceOf(NotFoundException.class); + } + + // ── getPolitique ────────────────────────────────────────────────────────── + + @Test + @DisplayName("getPolitique — politique existante → retourne la politique") + void getPolitique_existing() { + Organisation org = newOrg(); + ContactPolicy policy = ContactPolicy.builder() + .organisation(org) + .typePolitique(TypePolitiqueCommunication.BUREAU_SEULEMENT) + .autoriserMembreVersMembre(false) + .autoriserMembreVersRole(true) + .autoriserNotesVocales(true) + .build(); + policy.setId(UUID.randomUUID()); + + when(contactPolicyRepository.findByOrganisationId(ORG_ID)).thenReturn(Optional.of(policy)); + + ContactPolicyResponse result = messagingService.getPolitique(ORG_ID); + + assertThat(result.getTypePolitique()).isEqualTo("BUREAU_SEULEMENT"); + assertThat(result.isAutoriserMembreVersMembre()).isFalse(); + } + + // ── mettreAJourPolitique ────────────────────────────────────────────────── + + @Test + @DisplayName("mettreAJourPolitique — change typePolitique") + void mettreAJourPolitique_changeType() { + Organisation org = newOrg(); + ContactPolicy policy = ContactPolicy.builder() + .organisation(org) + .typePolitique(TypePolitiqueCommunication.OUVERT) + .autoriserMembreVersMembre(true) + .autoriserMembreVersRole(true) + .autoriserNotesVocales(true) + .build(); + policy.setId(UUID.randomUUID()); + + when(contactPolicyRepository.findByOrganisationId(ORG_ID)).thenReturn(Optional.of(policy)); + + MettreAJourPolitiqueRequest req = MettreAJourPolitiqueRequest.builder() + .typePolitique("BUREAU_SEULEMENT") + .autoriserMembreVersMembre(false) + .build(); + + ContactPolicyResponse result = messagingService.mettreAJourPolitique(ORG_ID, req); + + assertThat(result.getTypePolitique()).isEqualTo("BUREAU_SEULEMENT"); + assertThat(result.isAutoriserMembreVersMembre()).isFalse(); + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + private Membre newMembre(UUID id, String email, String prenom, String nom) { + Membre m = new Membre(); + m.setId(id); + m.setNumeroMembre("M-" + id.toString().substring(0, 4)); + m.setPrenom(prenom); + m.setNom(nom); + m.setEmail(email); + m.setDateNaissance(LocalDate.now()); + return m; + } + + private Organisation newOrg() { + Organisation org = new Organisation(); + org.setId(ORG_ID); + org.setNom("Tontine Test"); + return org; + } + + private Conversation buildConversation(TypeConversation type) { + Conversation conv = new Conversation(); + conv.setId(CONV_ID); + conv.setOrganisation(newOrg()); + conv.setTypeConversation(type); + conv.setStatut(StatutConversation.ACTIVE); + conv.setNombreMessages(0); + return conv; + } + + private Message buildMessage(Conversation conv) { + Membre expediteur = newMembre(MEMBRE_ID, "membre@test.com", "Alpha", "Diallo"); + Message msg = new Message(); + msg.setId(MESSAGE_ID); + msg.setConversation(conv); + msg.setExpediteur(expediteur); + msg.setTypeMessage(TypeContenu.TEXTE); + msg.setContenu("Bonjour !"); + msg.setDateCreation(LocalDateTime.now()); + return msg; + } + + @SuppressWarnings("unchecked") + private io.quarkus.hibernate.orm.panache.PanacheQuery mockSingleResultQuery(T entity) { + io.quarkus.hibernate.orm.panache.PanacheQuery query = + org.mockito.Mockito.mock(io.quarkus.hibernate.orm.panache.PanacheQuery.class); + when(query.firstResult()).thenReturn(entity); + return query; + } +} diff --git a/src/test/java/dev/lions/unionflow/server/service/VersementServiceTest.java b/src/test/java/dev/lions/unionflow/server/service/VersementServiceTest.java new file mode 100644 index 0000000..493a070 --- /dev/null +++ b/src/test/java/dev/lions/unionflow/server/service/VersementServiceTest.java @@ -0,0 +1,755 @@ +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.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import dev.lions.unionflow.server.api.dto.versement.request.DeclarerVersementManuelRequest; +import dev.lions.unionflow.server.api.dto.versement.request.InitierDepotEpargneRequest; +import dev.lions.unionflow.server.api.dto.versement.request.InitierVersementWaveRequest; +import dev.lions.unionflow.server.api.dto.versement.response.VersementGatewayResponse; +import dev.lions.unionflow.server.api.dto.versement.response.VersementResponse; +import dev.lions.unionflow.server.api.dto.versement.response.VersementStatutResponse; +import dev.lions.unionflow.server.api.dto.versement.response.VersementSummaryResponse; +import dev.lions.unionflow.server.api.enums.paiement.StatutIntentionPaiement; +import dev.lions.unionflow.server.entity.Cotisation; +import dev.lions.unionflow.server.entity.IntentionPaiement; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.entity.Versement; +import dev.lions.unionflow.server.entity.mutuelle.epargne.CompteEpargne; +import dev.lions.unionflow.server.repository.IntentionPaiementRepository; +import dev.lions.unionflow.server.repository.MembreOrganisationRepository; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.TypeReferenceRepository; +import dev.lions.unionflow.server.repository.VersementRepository; +import dev.lions.unionflow.server.repository.mutuelle.epargne.CompteEpargneRepository; +import dev.lions.unionflow.server.service.WaveCheckoutService.WaveCheckoutException; +import dev.lions.unionflow.server.service.WaveCheckoutService.WaveCheckoutSessionResponse; +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import jakarta.ws.rs.NotFoundException; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +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 unitaires pour {@link VersementService}. + * + * Couverture : trouverParId, trouverParNumeroReference, listerParMembre, + * calculerMontantTotalConfirmes, getMesVersements, validerVersement, + * annulerVersement, initierVersementWave (succès + Wave error + cotisation + * inconnue + cotisation d'un autre membre), declarerVersementManuel, + * verifierStatutVersement (complété / expiré / en attente), + * confirmerVersementWave (idempotence), toE164 (toutes branches). + */ +@QuarkusTest +@DisplayName("VersementService") +class VersementServiceTest { + + @Inject + VersementService versementService; + + @InjectMock VersementRepository versementRepository; + @InjectMock MembreRepository membreRepository; + @InjectMock KeycloakService keycloakService; + @InjectMock TypeReferenceRepository typeReferenceRepository; + @InjectMock IntentionPaiementRepository intentionPaiementRepository; + @InjectMock WaveCheckoutService waveCheckoutService; + @InjectMock CompteEpargneRepository compteEpargneRepository; + @InjectMock MembreOrganisationRepository membreOrganisationRepository; + @InjectMock NotificationService notificationService; + + private Membre testMembre; + private Membre autreMembre; + private Organisation testOrg; + private Cotisation testCotisation; + private Versement testVersement; + private CompteEpargne testCompte; + private EntityManager mockEm; + + @BeforeEach + void setUp() { + testOrg = new Organisation(); + testOrg.setId(UUID.randomUUID()); + testOrg.setNom("Org Test"); + + testMembre = new Membre(); + testMembre.setId(UUID.randomUUID()); + testMembre.setNumeroMembre("M001"); + testMembre.setEmail("membre@test.com"); + testMembre.setPrenom("Jean"); + testMembre.setNom("Dupont"); + testMembre.setDateNaissance(LocalDate.of(1990, 1, 1)); + + autreMembre = new Membre(); + autreMembre.setId(UUID.randomUUID()); + autreMembre.setNumeroMembre("M002"); + autreMembre.setEmail("autre@test.com"); + autreMembre.setDateNaissance(LocalDate.of(1985, 5, 15)); + + testCotisation = new Cotisation(); + testCotisation.setId(UUID.randomUUID()); + testCotisation.setNumeroReference("COT-2026-001"); + testCotisation.setMembre(testMembre); + testCotisation.setOrganisation(testOrg); + testCotisation.setMontantDu(new BigDecimal("5000")); + testCotisation.setCodeDevise("XOF"); + testCotisation.setStatut("EN_ATTENTE"); + testCotisation.setDateEcheance(LocalDate.now().plusDays(30)); + testCotisation.setTypeCotisation("MENSUELLE"); + testCotisation.setLibelle("Cotisation janvier 2026"); + testCotisation.setAnnee(2026); + + testVersement = new Versement(); + testVersement.setId(UUID.randomUUID()); + testVersement.setNumeroReference("VRS-2026-001"); + testVersement.setMontant(new BigDecimal("5000")); + testVersement.setCodeDevise("XOF"); + testVersement.setMethodePaiement("WAVE"); + testVersement.setStatutPaiement("EN_ATTENTE"); + testVersement.setMembre(testMembre); + testVersement.setDatePaiement(LocalDateTime.now()); + + testCompte = new CompteEpargne(); + testCompte.setId(UUID.randomUUID()); + testCompte.setMembre(testMembre); + testCompte.setOrganisation(testOrg); + + mockEm = mock(EntityManager.class); + when(versementRepository.getEntityManager()).thenReturn(mockEm); + when(typeReferenceRepository.findByDomaineAndCode(anyString(), anyString())) + .thenReturn(Optional.empty()); + when(keycloakService.getCurrentUserEmail()).thenReturn(testMembre.getEmail()); + } + + // ── trouverParId ────────────────────────────────────────────────────────── + + @Test + @DisplayName("trouverParId — trouvé → retourne response") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void trouverParId_found() { + when(versementRepository.findVersementById(testVersement.getId())) + .thenReturn(Optional.of(testVersement)); + + VersementResponse r = versementService.trouverParId(testVersement.getId()); + + assertThat(r.getNumeroReference()).isEqualTo("VRS-2026-001"); + } + + @Test + @DisplayName("trouverParId — non trouvé → NotFoundException") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void trouverParId_notFound() { + when(versementRepository.findVersementById(any())).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> versementService.trouverParId(UUID.randomUUID())) + .isInstanceOf(NotFoundException.class); + } + + // ── trouverParNumeroReference ───────────────────────────────────────────── + + @Test + @DisplayName("trouverParNumeroReference — trouvé → response") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void trouverParNumeroReference_found() { + when(versementRepository.findByNumeroReference("VRS-2026-001")) + .thenReturn(Optional.of(testVersement)); + + VersementResponse r = versementService.trouverParNumeroReference("VRS-2026-001"); + assertThat(r).isNotNull(); + } + + @Test + @DisplayName("trouverParNumeroReference — non trouvé → NotFoundException") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void trouverParNumeroReference_notFound() { + when(versementRepository.findByNumeroReference(anyString())).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> versementService.trouverParNumeroReference("VRS-INCONNU")) + .isInstanceOf(NotFoundException.class); + } + + // ── listerParMembre ─────────────────────────────────────────────────────── + + @Test + @DisplayName("listerParMembre — retourne liste non nulle") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void listerParMembre_returnsList() { + when(versementRepository.findByMembreId(testMembre.getId())) + .thenReturn(List.of(testVersement)); + + List list = versementService.listerParMembre(testMembre.getId()); + assertThat(list).hasSize(1); + } + + // ── calculerMontantTotalConfirmes ───────────────────────────────────────── + + @Test + @DisplayName("calculerMontantTotalConfirmes — délègue au repository") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void calculerMontantTotalConfirmes_delegates() { + LocalDateTime debut = LocalDateTime.now().minusDays(30); + LocalDateTime fin = LocalDateTime.now(); + when(versementRepository.calculerMontantTotalConfirmes(debut, fin)) + .thenReturn(new BigDecimal("15000")); + + BigDecimal total = versementService.calculerMontantTotalConfirmes(debut, fin); + assertThat(total).isEqualByComparingTo("15000"); + } + + // ── getMesVersements ────────────────────────────────────────────────────── + + @Test + @DisplayName("getMesVersements — retourne historique du membre connecté") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void getMesVersements_returnsHistory() { + when(membreRepository.findByEmail("membre@test.com")) + .thenReturn(Optional.of(testMembre)); + @SuppressWarnings("unchecked") + TypedQuery query = mock(TypedQuery.class); + doReturn(query).when(mockEm).createQuery(anyString(), any()); + when(query.setParameter(anyString(), any())).thenReturn(query); + when(query.setMaxResults(anyInt())).thenReturn(query); + when(query.getResultList()).thenReturn(List.of(testVersement)); + + List list = versementService.getMesVersements(5); + assertThat(list).hasSize(1); + } + + @Test + @DisplayName("getMesVersements — membre non trouvé → NotFoundException") + @TestSecurity(user = "inconnu@test.com", roles = {"MEMBRE"}) + void getMesVersements_memberNotFound() { + when(membreRepository.findByEmail(anyString())).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> versementService.getMesVersements(5)) + .isInstanceOf(NotFoundException.class); + } + + // ── validerVersement ────────────────────────────────────────────────────── + + @Test + @DisplayName("validerVersement — passe à CONFIRME") + @TestSecurity(user = "admin@test.com", roles = {"ADMIN"}) + void validerVersement_setsConfirme() { + when(versementRepository.findVersementById(testVersement.getId())) + .thenReturn(Optional.of(testVersement)); + + VersementResponse r = versementService.validerVersement(testVersement.getId()); + assertThat(r.getStatutPaiement()).isEqualTo("CONFIRME"); + } + + @Test + @DisplayName("validerVersement — déjà confirmé → idempotent") + @TestSecurity(user = "admin@test.com", roles = {"ADMIN"}) + void validerVersement_alreadyConfirme_idempotent() { + testVersement.setStatutPaiement("CONFIRME"); + when(versementRepository.findVersementById(testVersement.getId())) + .thenReturn(Optional.of(testVersement)); + + VersementResponse r = versementService.validerVersement(testVersement.getId()); + assertThat(r.getStatutPaiement()).isEqualTo("CONFIRME"); + } + + @Test + @DisplayName("validerVersement — non trouvé → NotFoundException") + @TestSecurity(user = "admin@test.com", roles = {"ADMIN"}) + void validerVersement_notFound() { + when(versementRepository.findVersementById(any())).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> versementService.validerVersement(UUID.randomUUID())) + .isInstanceOf(NotFoundException.class); + } + + // ── annulerVersement ────────────────────────────────────────────────────── + + @Test + @DisplayName("annulerVersement — passe à ANNULE") + @TestSecurity(user = "admin@test.com", roles = {"ADMIN"}) + void annulerVersement_setsAnnule() { + when(versementRepository.findVersementById(testVersement.getId())) + .thenReturn(Optional.of(testVersement)); + + VersementResponse r = versementService.annulerVersement(testVersement.getId()); + assertThat(r.getStatutPaiement()).isEqualTo("ANNULE"); + } + + @Test + @DisplayName("annulerVersement — déjà ANNULE → IllegalStateException") + @TestSecurity(user = "admin@test.com", roles = {"ADMIN"}) + void annulerVersement_alreadyAnnule() { + testVersement.setStatutPaiement("ANNULE"); + when(versementRepository.findVersementById(testVersement.getId())) + .thenReturn(Optional.of(testVersement)); + + assertThatThrownBy(() -> versementService.annulerVersement(testVersement.getId())) + .isInstanceOf(IllegalStateException.class); + } + + // ── initierVersementWave ────────────────────────────────────────────────── + + @Test + @DisplayName("initierVersementWave — succès Wave → retourne waveLaunchUrl") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void initierVersementWave_success() throws WaveCheckoutException { + when(membreRepository.findByEmail("membre@test.com")) + .thenReturn(Optional.of(testMembre)); + when(mockEm.find(Cotisation.class, testCotisation.getId())) + .thenReturn(testCotisation); + when(waveCheckoutService.getRedirectBaseUrl()).thenReturn("https://api.test.com"); + + WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse("cos-abc123", "wave://checkout/abc123"); + when(waveCheckoutService.createSession( + anyString(), anyString(), anyString(), anyString(), anyString(), anyString())) + .thenReturn(session); + doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class)); + doAnswer(inv -> null).when(versementRepository).persist(any(Versement.class)); + when(mockEm.merge(any())).thenReturn(testCotisation); + + InitierVersementWaveRequest request = InitierVersementWaveRequest.builder() + .cotisationId(testCotisation.getId()) + .numeroTelephone("771234567") + .build(); + + VersementGatewayResponse r = versementService.initierVersementWave(request); + + assertThat(r.getWaveLaunchUrl()).isEqualTo("wave://checkout/abc123"); + assertThat(r.getWaveCheckoutSessionId()).isEqualTo("cos-abc123"); + assertThat(r.getMontant()).isEqualByComparingTo("5000"); + assertThat(r.getClientReference()).isNotNull(); + } + + @Test + @DisplayName("initierVersementWave — cotisation inconnue → NotFoundException") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void initierVersementWave_cotisationInconnue() { + when(membreRepository.findByEmail("membre@test.com")) + .thenReturn(Optional.of(testMembre)); + when(mockEm.find(Cotisation.class, any())).thenReturn(null); + + InitierVersementWaveRequest request = InitierVersementWaveRequest.builder() + .cotisationId(UUID.randomUUID()) + .numeroTelephone("771234567") + .build(); + + assertThatThrownBy(() -> versementService.initierVersementWave(request)) + .isInstanceOf(NotFoundException.class); + } + + @Test + @DisplayName("initierVersementWave — cotisation d'un autre membre → IllegalArgumentException") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void initierVersementWave_cotisationAutreMembre() { + when(membreRepository.findByEmail("membre@test.com")) + .thenReturn(Optional.of(testMembre)); + testCotisation.setMembre(autreMembre); + when(mockEm.find(Cotisation.class, testCotisation.getId())) + .thenReturn(testCotisation); + + InitierVersementWaveRequest request = InitierVersementWaveRequest.builder() + .cotisationId(testCotisation.getId()) + .numeroTelephone("771234567") + .build(); + + assertThatThrownBy(() -> versementService.initierVersementWave(request)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("initierVersementWave — Wave API error → BadRequestException") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void initierVersementWave_waveApiError() throws WaveCheckoutException { + when(membreRepository.findByEmail("membre@test.com")) + .thenReturn(Optional.of(testMembre)); + when(mockEm.find(Cotisation.class, testCotisation.getId())) + .thenReturn(testCotisation); + when(waveCheckoutService.getRedirectBaseUrl()).thenReturn("https://api.test.com"); + doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class)); + doThrow(new WaveCheckoutException("Wave down")) + .when(waveCheckoutService).createSession( + anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); + + InitierVersementWaveRequest request = InitierVersementWaveRequest.builder() + .cotisationId(testCotisation.getId()) + .numeroTelephone("771234567") + .build(); + + assertThatThrownBy(() -> versementService.initierVersementWave(request)) + .isInstanceOf(jakarta.ws.rs.BadRequestException.class); + } + + @Test + @DisplayName("initierVersementWave — sans numéro téléphone (web QR) → successUrl web") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void initierVersementWave_noPhone_webContext() throws WaveCheckoutException { + when(membreRepository.findByEmail("membre@test.com")) + .thenReturn(Optional.of(testMembre)); + when(mockEm.find(Cotisation.class, testCotisation.getId())) + .thenReturn(testCotisation); + when(waveCheckoutService.getRedirectBaseUrl()).thenReturn("https://api.test.com"); + + WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse("cos-web", "wave://checkout/web"); + when(waveCheckoutService.createSession( + anyString(), anyString(), anyString(), anyString(), anyString(), anyString())) + .thenReturn(session); + doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class)); + doAnswer(inv -> null).when(versementRepository).persist(any(Versement.class)); + when(mockEm.merge(any())).thenReturn(testCotisation); + + InitierVersementWaveRequest request = InitierVersementWaveRequest.builder() + .cotisationId(testCotisation.getId()) + .build(); // pas de numéro → web + + VersementGatewayResponse r = versementService.initierVersementWave(request); + assertThat(r.getWaveLaunchUrl()).isEqualTo("wave://checkout/web"); + } + + // ── initierDepotEpargneEnLigne ──────────────────────────────────────────── + + @Test + @DisplayName("initierDepotEpargneEnLigne — succès Wave") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void initierDepotEpargne_success() throws WaveCheckoutException { + when(membreRepository.findByEmail("membre@test.com")) + .thenReturn(Optional.of(testMembre)); + when(compteEpargneRepository.findByIdOptional(testCompte.getId())) + .thenReturn(Optional.of(testCompte)); + when(waveCheckoutService.getRedirectBaseUrl()).thenReturn("https://api.test.com"); + + WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse("cos-epargne", "wave://checkout/epargne"); + when(waveCheckoutService.createSession( + anyString(), anyString(), anyString(), anyString(), anyString(), anyString())) + .thenReturn(session); + doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class)); + + InitierDepotEpargneRequest request = InitierDepotEpargneRequest.builder() + .compteId(testCompte.getId()) + .montant(new BigDecimal("10000")) + .numeroTelephone("771234567") + .build(); + + VersementGatewayResponse r = versementService.initierDepotEpargneEnLigne(request); + assertThat(r.getWaveLaunchUrl()).isEqualTo("wave://checkout/epargne"); + assertThat(r.getMontant()).isEqualByComparingTo("10000"); + } + + @Test + @DisplayName("initierDepotEpargneEnLigne — compte inconnu → NotFoundException") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void initierDepotEpargne_compteInconnu() { + when(membreRepository.findByEmail("membre@test.com")) + .thenReturn(Optional.of(testMembre)); + when(compteEpargneRepository.findByIdOptional(any())).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> versementService.initierDepotEpargneEnLigne( + InitierDepotEpargneRequest.builder() + .compteId(UUID.randomUUID()) + .montant(BigDecimal.TEN) + .build())) + .isInstanceOf(NotFoundException.class); + } + + @Test + @DisplayName("initierDepotEpargneEnLigne — compte d'un autre membre → IllegalArgumentException") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void initierDepotEpargne_compteAutreMembre() { + when(membreRepository.findByEmail("membre@test.com")) + .thenReturn(Optional.of(testMembre)); + testCompte.setMembre(autreMembre); + when(compteEpargneRepository.findByIdOptional(testCompte.getId())) + .thenReturn(Optional.of(testCompte)); + + assertThatThrownBy(() -> versementService.initierDepotEpargneEnLigne( + InitierDepotEpargneRequest.builder() + .compteId(testCompte.getId()) + .montant(BigDecimal.TEN) + .build())) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("initierDepotEpargneEnLigne — Wave error → BadRequestException") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void initierDepotEpargne_waveError() throws WaveCheckoutException { + when(membreRepository.findByEmail("membre@test.com")) + .thenReturn(Optional.of(testMembre)); + when(compteEpargneRepository.findByIdOptional(testCompte.getId())) + .thenReturn(Optional.of(testCompte)); + when(waveCheckoutService.getRedirectBaseUrl()).thenReturn("https://api.test.com"); + doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class)); + doThrow(new WaveCheckoutException("Wave down")) + .when(waveCheckoutService).createSession( + anyString(), anyString(), anyString(), anyString(), anyString(), anyString()); + + assertThatThrownBy(() -> versementService.initierDepotEpargneEnLigne( + InitierDepotEpargneRequest.builder() + .compteId(testCompte.getId()) + .montant(BigDecimal.TEN) + .build())) + .isInstanceOf(jakarta.ws.rs.BadRequestException.class); + } + + // ── declarerVersementManuel ─────────────────────────────────────────────── + + @Test + @DisplayName("declarerVersementManuel — créé EN_ATTENTE_VALIDATION") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void declarerVersementManuel_success() { + when(membreRepository.findByEmail("membre@test.com")) + .thenReturn(Optional.of(testMembre)); + @SuppressWarnings("unchecked") + TypedQuery query = mock(TypedQuery.class); + doReturn(query).when(mockEm).createQuery(anyString(), any()); + when(query.setParameter(anyString(), any())).thenReturn(query); + when(query.getResultList()).thenReturn(List.of(testCotisation)); + doAnswer(inv -> null).when(versementRepository).persist(any(Versement.class)); + when(membreOrganisationRepository.findFirstByMembreId(any())) + .thenReturn(Optional.empty()); + + DeclarerVersementManuelRequest request = DeclarerVersementManuelRequest.builder() + .cotisationId(testCotisation.getId()) + .methodePaiement("ESPECES") + .commentaire("Remis en main propre") + .build(); + + VersementResponse r = versementService.declarerVersementManuel(request); + assertThat(r.getStatutPaiement()).isEqualTo("EN_ATTENTE_VALIDATION"); + assertThat(r.getMethodePaiement()).isEqualTo("ESPECES"); + } + + @Test + @DisplayName("declarerVersementManuel — cotisation inconnue → NotFoundException") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void declarerVersementManuel_cotisationInconnue() { + when(membreRepository.findByEmail("membre@test.com")) + .thenReturn(Optional.of(testMembre)); + @SuppressWarnings("unchecked") + TypedQuery query = mock(TypedQuery.class); + doReturn(query).when(mockEm).createQuery(anyString(), any()); + when(query.setParameter(anyString(), any())).thenReturn(query); + when(query.getResultList()).thenReturn(Collections.emptyList()); + + assertThatThrownBy(() -> versementService.declarerVersementManuel( + DeclarerVersementManuelRequest.builder() + .cotisationId(UUID.randomUUID()) + .methodePaiement("ESPECES") + .build())) + .isInstanceOf(NotFoundException.class); + } + + @Test + @DisplayName("declarerVersementManuel — cotisation d'un autre membre → IllegalArgumentException") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void declarerVersementManuel_cotisationAutreMembre() { + when(membreRepository.findByEmail("membre@test.com")) + .thenReturn(Optional.of(testMembre)); + testCotisation.setMembre(autreMembre); + @SuppressWarnings("unchecked") + TypedQuery query = mock(TypedQuery.class); + doReturn(query).when(mockEm).createQuery(anyString(), any()); + when(query.setParameter(anyString(), any())).thenReturn(query); + when(query.getResultList()).thenReturn(List.of(testCotisation)); + + assertThatThrownBy(() -> versementService.declarerVersementManuel( + DeclarerVersementManuelRequest.builder() + .cotisationId(testCotisation.getId()) + .methodePaiement("ESPECES") + .build())) + .isInstanceOf(IllegalArgumentException.class); + } + + // ── verifierStatutVersement ─────────────────────────────────────────────── + + @Test + @DisplayName("verifierStatutVersement — intention inconnue → NotFoundException") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void verifierStatutVersement_notFound() { + when(intentionPaiementRepository.findById(any())).thenReturn(null); + + assertThatThrownBy(() -> versementService.verifierStatutVersement(UUID.randomUUID())) + .isInstanceOf(NotFoundException.class); + } + + @Test + @DisplayName("verifierStatutVersement — déjà COMPLETEE → confirme=true") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void verifierStatutVersement_completee() { + IntentionPaiement intention = buildIntention(StatutIntentionPaiement.COMPLETEE); + when(intentionPaiementRepository.findById(intention.getId())) + .thenReturn(intention); + + VersementStatutResponse r = versementService.verifierStatutVersement(intention.getId()); + assertThat(r.isConfirme()).isTrue(); + assertThat(r.getMessage()).contains("confirmé"); + } + + @Test + @DisplayName("verifierStatutVersement — EXPIREE → confirme=false") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void verifierStatutVersement_expiree() { + IntentionPaiement intention = buildIntention(StatutIntentionPaiement.EXPIREE); + when(intentionPaiementRepository.findById(intention.getId())) + .thenReturn(intention); + + VersementStatutResponse r = versementService.verifierStatutVersement(intention.getId()); + assertThat(r.isConfirme()).isFalse(); + } + + @Test + @DisplayName("verifierStatutVersement — ECHOUEE → confirme=false") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void verifierStatutVersement_echouee() { + IntentionPaiement intention = buildIntention(StatutIntentionPaiement.ECHOUEE); + when(intentionPaiementRepository.findById(intention.getId())) + .thenReturn(intention); + + VersementStatutResponse r = versementService.verifierStatutVersement(intention.getId()); + assertThat(r.isConfirme()).isFalse(); + } + + @Test + @DisplayName("verifierStatutVersement — session expirée localement → EXPIREE") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void verifierStatutVersement_localExpiry() { + IntentionPaiement intention = buildIntention(StatutIntentionPaiement.EN_COURS); + intention.setDateExpiration(LocalDateTime.now().minusMinutes(5)); + when(intentionPaiementRepository.findById(intention.getId())) + .thenReturn(intention); + + VersementStatutResponse r = versementService.verifierStatutVersement(intention.getId()); + assertThat(r.getStatut()).isEqualTo("EXPIREE"); + } + + @Test + @DisplayName("verifierStatutVersement — EN_COURS sans session Wave → en attente") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void verifierStatutVersement_enCoursNoSession() { + IntentionPaiement intention = buildIntention(StatutIntentionPaiement.EN_COURS); + intention.setDateExpiration(LocalDateTime.now().plusMinutes(25)); + when(intentionPaiementRepository.findById(intention.getId())) + .thenReturn(intention); + + VersementStatutResponse r = versementService.verifierStatutVersement(intention.getId()); + assertThat(r.isConfirme()).isFalse(); + assertThat(r.getMessage()).containsIgnoringCase("attente"); + } + + // ── confirmerVersementWave ──────────────────────────────────────────────── + + @Test + @DisplayName("confirmerVersementWave — déjà COMPLETEE → idempotent") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void confirmerVersementWave_alreadyCompletee_idempotent() { + IntentionPaiement intention = buildIntention(StatutIntentionPaiement.COMPLETEE); + // Ne doit pas appeler persist + versementService.confirmerVersementWave(intention, null); + // Pas d'exception = idempotence OK + } + + @Test + @DisplayName("confirmerVersementWave — objetsCibles null → passe sans erreur") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void confirmerVersementWave_nullObjetsCibles() { + IntentionPaiement intention = buildIntention(StatutIntentionPaiement.EN_COURS); + intention.setObjetsCibles(null); + doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class)); + + versementService.confirmerVersementWave(intention, "TCN-123"); + + assertThat(intention.getStatut()).isEqualTo(StatutIntentionPaiement.COMPLETEE); + } + + @Test + @DisplayName("confirmerVersementWave — JSON cotisation → mise à jour cotisation") + @TestSecurity(user = "membre@test.com", roles = {"MEMBRE"}) + void confirmerVersementWave_reconcilesCotisation() { + UUID cotisationId = testCotisation.getId(); + IntentionPaiement intention = buildIntention(StatutIntentionPaiement.EN_COURS); + intention.setObjetsCibles( + "[{\"type\":\"COTISATION\",\"id\":\"" + cotisationId + "\",\"montant\":5000}]"); + doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class)); + when(mockEm.find(Cotisation.class, cotisationId)).thenReturn(testCotisation); + when(mockEm.merge(any())).thenReturn(testCotisation); + + versementService.confirmerVersementWave(intention, "TCN-ABC"); + + assertThat(testCotisation.getStatut()).isEqualTo("PAYEE"); + assertThat(testCotisation.getMontantPaye()).isEqualByComparingTo("5000"); + } + + // ── toE164 ──────────────────────────────────────────────────────────────── + + @Test + @DisplayName("toE164 — null → null") + void toE164_null() { + assertThat(VersementService.toE164(null)).isNull(); + } + + @Test + @DisplayName("toE164 — vide → null") + void toE164_blank() { + assertThat(VersementService.toE164(" ")).isNull(); + } + + @Test + @DisplayName("toE164 — 9 chiffres commençant par 7 → +221 prefix") + void toE164_9digits_7prefix() { + assertThat(VersementService.toE164("771234567")).isEqualTo("+221771234567"); + } + + @Test + @DisplayName("toE164 — 9 chiffres commençant par 0 → +221 + supprime 0") + void toE164_9digits_0prefix() { + assertThat(VersementService.toE164("071234567")).isEqualTo("+22171234567"); + } + + @Test + @DisplayName("toE164 — déjà avec indicatif 221 → ajoute +") + void toE164_with221prefix() { + assertThat(VersementService.toE164("221771234567")).isEqualTo("+221771234567"); + } + + @Test + @DisplayName("toE164 — déjà au format E.164 (+221...) → conservé") + void toE164_alreadyE164() { + assertThat(VersementService.toE164("+221771234567")).isEqualTo("+221771234567"); + } + + @Test + @DisplayName("toE164 — format non reconnu → + + chiffres") + void toE164_unknown() { + assertThat(VersementService.toE164("0033612345678")).startsWith("+"); + } + + // ── helpers ─────────────────────────────────────────────────────────────── + + private IntentionPaiement buildIntention(StatutIntentionPaiement statut) { + IntentionPaiement i = new IntentionPaiement(); + i.setId(UUID.randomUUID()); + i.setStatut(statut); + i.setMontantTotal(new BigDecimal("5000")); + i.setCodeDevise("XOF"); + i.setUtilisateur(testMembre); + i.setDateExpiration(LocalDateTime.now().plusMinutes(30)); + return i; + } +}