fix(security): audit RBAC complet v3.0 — rôles normalisés, lifecycle, changement mdp mobile
RBAC:
- HealthResource: @PermitAll
- RoleResource: @RolesAllowed ADMIN/SUPER_ADMIN/ADMIN_ORGANISATION class-level
- PropositionAideResource: @RolesAllowed MEMBRE/USER class-level
- AuthCallbackResource: @PermitAll
- EvenementResource: @PermitAll /publics et /test, count restreint
- BackupResource/LogsMonitoringResource/SystemResource: MODERATOR → MODERATEUR
- AnalyticsResource: MANAGER/MEMBER → ADMIN_ORGANISATION/MEMBRE
- RoleConstant.java: constantes de rôles centralisées
Cycle de vie membres:
- MemberLifecycleService: ajouterMembre()/retirerMembre() sur activation/radiation/archivage
- MembreResource: endpoint GET /numero/{numeroMembre}
- MembreService: méthode trouverParNumeroMembre()
Changement mot de passe:
- CompteAdherentResource: endpoint POST /auth/change-password (mobile)
- MembreKeycloakSyncService: changerMotDePasseDirectKeycloak() via API Admin Keycloak directe
- Fallback automatique si lions-user-manager indisponible
Workflow:
- Flyway V17-V23: rôles, types org, formules Option C, lifecycle columns, bareme cotisation
- Nouvelles classes: MemberLifecycleService, OrganisationModuleService, scheduler
- Security: OrganisationContextFilter, OrganisationContextHolder, ModuleAccessFilter
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import dev.lions.unionflow.server.api.enums.abonnement.PlageMembres;
|
||||
import dev.lions.unionflow.server.api.enums.abonnement.StatutSouscription;
|
||||
import dev.lions.unionflow.server.api.enums.abonnement.TypeFormule;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour la logique de quota (Option C) et les champs de plan commercial.
|
||||
*
|
||||
* <p>Couvre :
|
||||
* <ul>
|
||||
* <li>{@link SouscriptionOrganisation#isActive()} — selon statut + dateFin</li>
|
||||
* <li>{@link SouscriptionOrganisation#isQuotaDepasse()} — selon quotaMax et quotaUtilise</li>
|
||||
* <li>{@link SouscriptionOrganisation#getPlacesRestantes()} — calcul des places libres</li>
|
||||
* <li>{@link FormuleAbonnement#isIllimitee()} — maxMembres null = illimité</li>
|
||||
* <li>{@link FormuleAbonnement#accepteNouveauMembre(int)} — contrôle admission</li>
|
||||
* <li>Champs Option C (planCommercial, apiAccess, federationAccess, maxAdmins)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@DisplayName("SouscriptionOrganisation + FormuleAbonnement — Quota & Option C")
|
||||
class SouscriptionQuotaOptionCTest {
|
||||
|
||||
// ─── Builders ─────────────────────────────────────────────────────────────
|
||||
|
||||
private static Organisation newOrganisation() {
|
||||
Organisation o = new Organisation();
|
||||
o.setId(UUID.randomUUID());
|
||||
o.setNom("Association Test");
|
||||
o.setTypeOrganisation("ASSOCIATION");
|
||||
o.setStatut("ACTIVE");
|
||||
o.setEmail("test@asso.sn");
|
||||
return o;
|
||||
}
|
||||
|
||||
private static FormuleAbonnement newFormule(int maxMembres) {
|
||||
FormuleAbonnement f = new FormuleAbonnement();
|
||||
f.setId(UUID.randomUUID());
|
||||
f.setCode(TypeFormule.STANDARD);
|
||||
f.setPlage(PlageMembres.PETITE);
|
||||
f.setLibelle("Standard Petite");
|
||||
f.setMaxMembres(maxMembres);
|
||||
f.setPrixMensuel(BigDecimal.valueOf(7500));
|
||||
f.setPrixAnnuel(BigDecimal.valueOf(75000));
|
||||
return f;
|
||||
}
|
||||
|
||||
private static FormuleAbonnement newFormuleIllimitee() {
|
||||
FormuleAbonnement f = new FormuleAbonnement();
|
||||
f.setId(UUID.randomUUID());
|
||||
f.setCode(TypeFormule.PREMIUM);
|
||||
f.setPlage(PlageMembres.GRANDE);
|
||||
f.setLibelle("Enterprise");
|
||||
f.setMaxMembres(null); // illimité
|
||||
f.setPrixMensuel(BigDecimal.valueOf(30000));
|
||||
f.setPrixAnnuel(BigDecimal.valueOf(300000));
|
||||
return f;
|
||||
}
|
||||
|
||||
private static SouscriptionOrganisation newSouscription(
|
||||
Organisation org, FormuleAbonnement formule,
|
||||
StatutSouscription statut, LocalDate dateFin,
|
||||
int quotaMax, int quotaUtilise) {
|
||||
|
||||
SouscriptionOrganisation s = new SouscriptionOrganisation();
|
||||
s.setId(UUID.randomUUID());
|
||||
s.setOrganisation(org);
|
||||
s.setFormule(formule);
|
||||
s.setStatut(statut);
|
||||
s.setDateDebut(LocalDate.now().minusMonths(1));
|
||||
s.setDateFin(dateFin);
|
||||
s.setQuotaMax(quotaMax);
|
||||
s.setQuotaUtilise(quotaUtilise);
|
||||
return s;
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// isActive()
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@DisplayName("isActive: statut ACTIVE et dateFin future → true")
|
||||
void isActive_activePlusFutureDateFin_returnsTrue() {
|
||||
var s = newSouscription(newOrganisation(), newFormule(100),
|
||||
StatutSouscription.ACTIVE, LocalDate.now().plusMonths(6), 100, 0);
|
||||
|
||||
assertThat(s.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("isActive: statut ACTIVE mais dateFin hier → false (expirée)")
|
||||
void isActive_activePlusExpiredDate_returnsFalse() {
|
||||
var s = newSouscription(newOrganisation(), newFormule(100),
|
||||
StatutSouscription.ACTIVE, LocalDate.now().minusDays(1), 100, 0);
|
||||
|
||||
assertThat(s.isActive()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("isActive: statut SUSPENDUE → false")
|
||||
void isActive_suspendue_returnsFalse() {
|
||||
var s = newSouscription(newOrganisation(), newFormule(100),
|
||||
StatutSouscription.SUSPENDUE, LocalDate.now().plusMonths(1), 100, 0);
|
||||
|
||||
assertThat(s.isActive()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("isActive: statut EXPIREE → false")
|
||||
void isActive_expiree_returnsFalse() {
|
||||
var s = newSouscription(newOrganisation(), newFormule(100),
|
||||
StatutSouscription.EXPIREE, LocalDate.now().plusMonths(1), 100, 0);
|
||||
|
||||
assertThat(s.isActive()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("isActive: dateFin aujourd'hui → true (borne inclusive)")
|
||||
void isActive_dateFin_today_returnsTrue() {
|
||||
var s = newSouscription(newOrganisation(), newFormule(100),
|
||||
StatutSouscription.ACTIVE, LocalDate.now(), 100, 0);
|
||||
|
||||
assertThat(s.isActive()).isTrue();
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// isQuotaDepasse()
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@DisplayName("isQuotaDepasse: quotaUtilise < quotaMax → false")
|
||||
void isQuotaDepasse_utiliseInferieur_returnsFalse() {
|
||||
var s = newSouscription(newOrganisation(), newFormule(100),
|
||||
StatutSouscription.ACTIVE, LocalDate.now().plusMonths(1), 100, 50);
|
||||
|
||||
assertThat(s.isQuotaDepasse()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("isQuotaDepasse: quotaUtilise == quotaMax → true (atteint)")
|
||||
void isQuotaDepasse_utiliseEgalMax_returnsTrue() {
|
||||
var s = newSouscription(newOrganisation(), newFormule(100),
|
||||
StatutSouscription.ACTIVE, LocalDate.now().plusMonths(1), 100, 100);
|
||||
|
||||
assertThat(s.isQuotaDepasse()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("isQuotaDepasse: quotaUtilise > quotaMax → true (dépassé)")
|
||||
void isQuotaDepasse_utiliseSuperieur_returnsTrue() {
|
||||
var s = newSouscription(newOrganisation(), newFormule(100),
|
||||
StatutSouscription.ACTIVE, LocalDate.now().plusMonths(1), 100, 110);
|
||||
|
||||
assertThat(s.isQuotaDepasse()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("isQuotaDepasse: quotaMax null (illimité) → false")
|
||||
void isQuotaDepasse_quotaMaxNull_returnsFalse() {
|
||||
var s = newSouscription(newOrganisation(), newFormuleIllimitee(),
|
||||
StatutSouscription.ACTIVE, LocalDate.now().plusYears(1), 0, 500);
|
||||
s.setQuotaMax(null); // explicitement illimité
|
||||
|
||||
assertThat(s.isQuotaDepasse()).isFalse();
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// getPlacesRestantes()
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@DisplayName("getPlacesRestantes: 100 max - 30 utilisés = 70 restantes")
|
||||
void getPlacesRestantes_partialUsage_returnsCorrectValue() {
|
||||
var s = newSouscription(newOrganisation(), newFormule(100),
|
||||
StatutSouscription.ACTIVE, LocalDate.now().plusMonths(1), 100, 30);
|
||||
|
||||
assertThat(s.getPlacesRestantes()).isEqualTo(70);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getPlacesRestantes: quota plein → 0 (jamais négatif)")
|
||||
void getPlacesRestantes_quotaFull_returnsZero() {
|
||||
var s = newSouscription(newOrganisation(), newFormule(100),
|
||||
StatutSouscription.ACTIVE, LocalDate.now().plusMonths(1), 100, 100);
|
||||
|
||||
assertThat(s.getPlacesRestantes()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getPlacesRestantes: quotaMax null (illimité) → Integer.MAX_VALUE")
|
||||
void getPlacesRestantes_illimite_returnsMaxValue() {
|
||||
var s = newSouscription(newOrganisation(), newFormuleIllimitee(),
|
||||
StatutSouscription.ACTIVE, LocalDate.now().plusYears(1), 0, 0);
|
||||
s.setQuotaMax(null);
|
||||
|
||||
assertThat(s.getPlacesRestantes()).isEqualTo(Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// FormuleAbonnement — isIllimitee() + accepteNouveauMembre()
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@DisplayName("FormuleAbonnement.isIllimitee: maxMembres null → true")
|
||||
void formule_maxMembresNull_isIllimitee() {
|
||||
var f = newFormuleIllimitee();
|
||||
assertThat(f.isIllimitee()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("FormuleAbonnement.isIllimitee: maxMembres défini → false")
|
||||
void formule_maxMembresDefined_notIllimitee() {
|
||||
var f = newFormule(200);
|
||||
assertThat(f.isIllimitee()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("FormuleAbonnement.accepteNouveauMembre: quota non atteint → true")
|
||||
void formule_accepteNouveauMembre_quotaNotReached_returnsTrue() {
|
||||
var f = newFormule(100);
|
||||
assertThat(f.accepteNouveauMembre(99)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("FormuleAbonnement.accepteNouveauMembre: quota atteint → false")
|
||||
void formule_accepteNouveauMembre_quotaReached_returnsFalse() {
|
||||
var f = newFormule(100);
|
||||
assertThat(f.accepteNouveauMembre(100)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("FormuleAbonnement.accepteNouveauMembre: formule illimitée → toujours true")
|
||||
void formule_accepteNouveauMembre_illimitee_alwaysTrue() {
|
||||
var f = newFormuleIllimitee();
|
||||
assertThat(f.accepteNouveauMembre(10000)).isTrue();
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// Champs Option C sur FormuleAbonnement
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@DisplayName("FormuleAbonnement Option C: plan ENTERPRISE — tous les droits activés")
|
||||
void formule_enterprisePlan_allFeaturesEnabled() {
|
||||
FormuleAbonnement f = new FormuleAbonnement();
|
||||
f.setCode(TypeFormule.PREMIUM);
|
||||
f.setPlage(PlageMembres.GRANDE);
|
||||
f.setLibelle("Enterprise");
|
||||
f.setMaxMembres(null);
|
||||
f.setPrixMensuel(BigDecimal.valueOf(50000));
|
||||
f.setPrixAnnuel(BigDecimal.valueOf(500000));
|
||||
f.setPlanCommercial("ENTERPRISE");
|
||||
f.setNiveauReporting("AVANCE");
|
||||
f.setApiAccess(true);
|
||||
f.setFederationAccess(true);
|
||||
f.setSupportPrioritaire(true);
|
||||
f.setSlaGaranti("99.9%");
|
||||
f.setMaxAdmins(null); // illimité
|
||||
|
||||
assertThat(f.getPlanCommercial()).isEqualTo("ENTERPRISE");
|
||||
assertThat(f.getNiveauReporting()).isEqualTo("AVANCE");
|
||||
assertThat(f.getApiAccess()).isTrue();
|
||||
assertThat(f.getFederationAccess()).isTrue();
|
||||
assertThat(f.getSupportPrioritaire()).isTrue();
|
||||
assertThat(f.getSlaGaranti()).isEqualTo("99.9%");
|
||||
assertThat(f.getMaxAdmins()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("FormuleAbonnement Option C: plan BASIC — accès limités")
|
||||
void formule_basicPlan_limitedFeatures() {
|
||||
FormuleAbonnement f = newFormule(50);
|
||||
f.setPlanCommercial("MICRO");
|
||||
f.setNiveauReporting("BASIQUE");
|
||||
f.setApiAccess(false);
|
||||
f.setFederationAccess(false);
|
||||
f.setSupportPrioritaire(false);
|
||||
f.setSlaGaranti("99.0%");
|
||||
f.setMaxAdmins(1);
|
||||
|
||||
assertThat(f.getApiAccess()).isFalse();
|
||||
assertThat(f.getFederationAccess()).isFalse();
|
||||
assertThat(f.getSupportPrioritaire()).isFalse();
|
||||
assertThat(f.getMaxAdmins()).isEqualTo(1);
|
||||
assertThat(f.getNiveauReporting()).isEqualTo("BASIQUE");
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// incrementerQuota() + decrementerQuota()
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@DisplayName("incrementerQuota: 30 utilisés → 31 après incrémentation")
|
||||
void incrementerQuota_increment() {
|
||||
var s = newSouscription(newOrganisation(), newFormule(100),
|
||||
StatutSouscription.ACTIVE, LocalDate.now().plusMonths(1), 100, 30);
|
||||
|
||||
s.incrementerQuota();
|
||||
|
||||
assertThat(s.getQuotaUtilise()).isEqualTo(31);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("decrementerQuota: 30 utilisés → 29 après décrémentation")
|
||||
void decrementerQuota_decrement() {
|
||||
var s = newSouscription(newOrganisation(), newFormule(100),
|
||||
StatutSouscription.ACTIVE, LocalDate.now().plusMonths(1), 100, 30);
|
||||
|
||||
s.decrementerQuota();
|
||||
|
||||
assertThat(s.getQuotaUtilise()).isEqualTo(29);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("decrementerQuota: 0 utilisés → reste à 0 (pas négatif)")
|
||||
void decrementerQuota_atZero_staysZero() {
|
||||
var s = newSouscription(newOrganisation(), newFormule(100),
|
||||
StatutSouscription.ACTIVE, LocalDate.now().plusMonths(1), 100, 0);
|
||||
|
||||
s.decrementerQuota();
|
||||
|
||||
assertThat(s.getQuotaUtilise()).isZero();
|
||||
}
|
||||
}
|
||||
@@ -225,6 +225,64 @@ class CompteAdherentResourceTest {
|
||||
verify(membreService, never()).activerMembre(any());
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Error cases
|
||||
// ============================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/membres/mon-compte sans authentification retourne 401")
|
||||
void getMonCompte_sansAuthentification_returns401() {
|
||||
given()
|
||||
.when()
|
||||
.get("/api/membres/mon-compte")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("GET /api/membres/mon-compte retourne 500 quand le service lève une exception")
|
||||
void getMonCompte_serviceException_returns500() {
|
||||
when(compteAdherentService.getMonCompte())
|
||||
.thenThrow(new RuntimeException("Erreur base de données"));
|
||||
|
||||
given()
|
||||
.when()
|
||||
.get("/api/membres/mon-compte")
|
||||
.then()
|
||||
.statusCode(500);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/membres/mon-statut sans authentification retourne 401")
|
||||
void getMonStatut_sansAuthentification_returns401() {
|
||||
given()
|
||||
.when()
|
||||
.get("/api/membres/mon-statut")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("GET /api/membres/mon-statut retourne EN_ATTENTE_VALIDATION quand aucune org trouvée")
|
||||
void getMonStatut_enAttenteMembreOrganisationAbsente_retourneEnAttente() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
|
||||
Membre membre = membreFixture("membre@test.com", "EN_ATTENTE_VALIDATION");
|
||||
membre.setId(membreId);
|
||||
|
||||
when(membreRepository.findByEmail("membre@test.com")).thenReturn(Optional.of(membre));
|
||||
when(membreOrganisationRepository.findFirstByMembreId(membreId)).thenReturn(Optional.empty());
|
||||
|
||||
given()
|
||||
.when()
|
||||
.get("/api/membres/mon-statut")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("statutCompte", equalTo("EN_ATTENTE_VALIDATION"));
|
||||
}
|
||||
|
||||
// ─── Helper ────────────────────────────────────────────────────────────────
|
||||
|
||||
private Membre membreFixture(String email, String statut) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationResponse;
|
||||
import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationSummaryResponse;
|
||||
import dev.lions.unionflow.server.service.CotisationService;
|
||||
import io.quarkus.test.InjectMock;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
@@ -520,12 +519,15 @@ class CotisationResourceMockTest {
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
|
||||
@DisplayName("GET /api/cotisations/public retourne 200 avec données mockées")
|
||||
void getCotisationsPublic_withMockedData_returns200() {
|
||||
CotisationSummaryResponse summary = new CotisationSummaryResponse(
|
||||
UUID.randomUUID(), "COT-001", "Jean Dupont",
|
||||
new BigDecimal("5000"), new BigDecimal("0"),
|
||||
"EN_ATTENTE", "En attente", LocalDate.now().plusMonths(1),
|
||||
LocalDate.now().getYear(), Boolean.TRUE
|
||||
);
|
||||
CotisationResponse summary = CotisationResponse.builder()
|
||||
.numeroReference("COT-001")
|
||||
.nomMembre("Jean Dupont")
|
||||
.montantDu(new BigDecimal("5000"))
|
||||
.montantPaye(new BigDecimal("0"))
|
||||
.statut("EN_ATTENTE")
|
||||
.statutLibelle("En attente")
|
||||
.dateEcheance(LocalDate.now().plusMonths(1))
|
||||
.build();
|
||||
when(cotisationService.getAllCotisations(anyInt(), anyInt()))
|
||||
.thenReturn(List.of(summary));
|
||||
when(cotisationService.getStatistiquesCotisations())
|
||||
@@ -552,12 +554,15 @@ class CotisationResourceMockTest {
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
|
||||
@DisplayName("GET /api/cotisations/public avec totalCotisations null utilise content.size() (branche ligne 82)")
|
||||
void getCotisationsPublic_totalCotisationsNull_usesContentSize() {
|
||||
CotisationSummaryResponse summary = new CotisationSummaryResponse(
|
||||
UUID.randomUUID(), "COT-002", "Marie Dupont",
|
||||
new BigDecimal("3000"), new BigDecimal("0"),
|
||||
"EN_ATTENTE", "En attente", LocalDate.now().plusMonths(2),
|
||||
LocalDate.now().getYear(), Boolean.FALSE
|
||||
);
|
||||
CotisationResponse summary = CotisationResponse.builder()
|
||||
.numeroReference("COT-002")
|
||||
.nomMembre("Marie Dupont")
|
||||
.montantDu(new BigDecimal("3000"))
|
||||
.montantPaye(new BigDecimal("0"))
|
||||
.statut("EN_ATTENTE")
|
||||
.statutLibelle("En attente")
|
||||
.dateEcheance(LocalDate.now().plusMonths(2))
|
||||
.build();
|
||||
when(cotisationService.getAllCotisations(anyInt(), anyInt()))
|
||||
.thenReturn(List.of(summary));
|
||||
// getStatistiquesCotisations() retourne une map SANS "totalCotisations"
|
||||
|
||||
@@ -66,4 +66,35 @@ class DashboardWebSocketEndpointTest {
|
||||
endpoint.onClose(connection);
|
||||
// pas d'exception attendue
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("onMessage avec message vide retourne ack")
|
||||
void onMessage_emptyMessage_returnsAck() {
|
||||
String result = endpoint.onMessage("", connection);
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result).contains("ack");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("onMessage avec null retourne une réponse non nulle")
|
||||
void onMessage_nullMessage_returnsNonNull() {
|
||||
String result = endpoint.onMessage(null, connection);
|
||||
assertThat(result).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("onOpen avec connection id null ne lève pas d'exception")
|
||||
void onOpen_connectionIdNull_doesNotThrow() {
|
||||
when(connection.id()).thenReturn(null);
|
||||
String result = endpoint.onOpen(connection);
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result).contains("connected");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("onMessage avec JSON inconnu retourne ack")
|
||||
void onMessage_unknownJson_returnsAck() {
|
||||
String result = endpoint.onMessage("{\"type\":\"unknown\",\"data\":{}}", connection);
|
||||
assertThat(result).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +155,60 @@ class DemandeAideMockResourceTest {
|
||||
when(membreRepository.findByEmail(anyString())).thenReturn(Optional.of(membre));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Error cases
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/demandes-aide/mes sans authentification retourne 401")
|
||||
void mesDemandes_sansAuthentification_returns401() {
|
||||
given()
|
||||
.when()
|
||||
.get("/api/demandes-aide/mes")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/demandes-aide sans authentification retourne 401")
|
||||
void listerToutes_sansAuthentification_returns401() {
|
||||
given()
|
||||
.when()
|
||||
.get("/api/demandes-aide")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("GET /api/demandes-aide/{id} avec ID inexistant retourne 404")
|
||||
void obtenirParId_notFound_returns404() {
|
||||
UUID id = UUID.randomUUID();
|
||||
when(demandeAideService.obtenirParId(any(UUID.class))).thenReturn(null);
|
||||
|
||||
given()
|
||||
.pathParam("id", id)
|
||||
.when()
|
||||
.get("/api/demandes-aide/{id}")
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("PUT /api/demandes-aide/{id}/approuver avec rôle MEMBRE retourne 403")
|
||||
void approuver_avecRoleMembre_returns403() {
|
||||
UUID id = UUID.randomUUID();
|
||||
|
||||
given()
|
||||
.queryParam("motif", "Test")
|
||||
.pathParam("id", id)
|
||||
.when()
|
||||
.put("/api/demandes-aide/{id}/approuver")
|
||||
.then()
|
||||
.statusCode(403);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// obtenirParId — response != null (found)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -251,6 +251,47 @@ class ExportResourceTest {
|
||||
.header("Content-Disposition", containsString("rapport-2026-03.pdf"));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Error cases
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/export/cotisations/csv sans authentification retourne 401")
|
||||
void exporterCotisationsCSV_sansAuthentification_returns401() {
|
||||
given()
|
||||
.when()
|
||||
.get("/api/export/cotisations/csv")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/export/rapport/mensuel sans authentification retourne 401")
|
||||
void genererRapportMensuel_sansAuthentification_returns401() {
|
||||
given()
|
||||
.queryParam("annee", 2026)
|
||||
.queryParam("mois", 3)
|
||||
.when()
|
||||
.get("/api/export/rapport/mensuel")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
|
||||
@DisplayName("GET /api/export/cotisations/{id}/recu/pdf quand service lève exception retourne 500")
|
||||
void genererRecuPDF_serviceException_returns500() {
|
||||
when(exportService.genererRecuPaiementPDF(org.mockito.ArgumentMatchers.any(UUID.class)))
|
||||
.thenThrow(new RuntimeException("Cotisation introuvable"));
|
||||
|
||||
given()
|
||||
.pathParam("cotisationId", COTISATION_ID)
|
||||
.when()
|
||||
.get("/api/export/cotisations/{cotisationId}/recu/pdf")
|
||||
.then()
|
||||
.statusCode(500);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
|
||||
@DisplayName("GET /api/export/rapport/mensuel/pdf avec associationId retourne 200")
|
||||
|
||||
@@ -208,4 +208,52 @@ class FinanceWorkflowResourceTest {
|
||||
.body("format", equalTo("csv"));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Error cases
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/finance/stats sans authentification retourne 401")
|
||||
void getStats_sansAuthentification_returns401() {
|
||||
given()
|
||||
.when()
|
||||
.get(BASE_PATH + "/stats")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/finance/audit-logs sans authentification retourne 401")
|
||||
void getAuditLogs_sansAuthentification_returns401() {
|
||||
given()
|
||||
.when()
|
||||
.get(BASE_PATH + "/audit-logs")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("GET /api/finance/stats avec rôle MEMBRE retourne 403")
|
||||
void getStats_avecRoleMembre_returns403() {
|
||||
given()
|
||||
.when()
|
||||
.get(BASE_PATH + "/stats")
|
||||
.then()
|
||||
.statusCode(403);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("POST /api/finance/audit-logs/export avec rôle MEMBRE retourne 403")
|
||||
void exportAuditLogs_avecRoleMembre_returns403() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"organizationId\":\"00000000-0000-0000-0000-000000000001\"}")
|
||||
.when()
|
||||
.post(BASE_PATH + "/audit-logs/export")
|
||||
.then()
|
||||
.statusCode(403);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,4 +26,38 @@ class HealthResourceTest {
|
||||
.body("timestamp", notNullValue())
|
||||
.body("message", equalTo("Serveur opérationnel"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/status avec Accept inconnu retourne 200 (endpoint toujours accessible)")
|
||||
void getStatus_acceptHeaderDifferent_returns200() {
|
||||
given()
|
||||
.header("Accept", "application/json")
|
||||
.when()
|
||||
.get("/api/status")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("status", equalTo("UP"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/status/inexistant retourne 404")
|
||||
void getStatusInexistant_returns404() {
|
||||
given()
|
||||
.when()
|
||||
.get("/api/status/inexistant")
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /api/status retourne 405 (méthode non autorisée)")
|
||||
void postStatus_returns405() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{}")
|
||||
.when()
|
||||
.post("/api/status")
|
||||
.then()
|
||||
.statusCode(405);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,4 +57,46 @@ class LogsMonitoringResourceCoverageTest {
|
||||
.statusCode(200)
|
||||
.body(containsString("Stack trace ligne 42"));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Error cases
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/logs/export sans authentification retourne 401")
|
||||
void exportLogs_sansAuthentification_returns401() {
|
||||
given()
|
||||
.queryParam("level", "ERROR")
|
||||
.when()
|
||||
.get("/api/logs/export")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("GET /api/logs/export avec rôle MEMBRE retourne 403")
|
||||
void exportLogs_avecRoleMembre_returns403() {
|
||||
given()
|
||||
.queryParam("level", "ERROR")
|
||||
.when()
|
||||
.get("/api/logs/export")
|
||||
.then()
|
||||
.statusCode(403);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
|
||||
@DisplayName("GET /api/logs/export quand service lève exception retourne 500")
|
||||
void exportLogs_serviceException_returns500() {
|
||||
when(logsMonitoringService.searchLogs(any(LogSearchRequest.class)))
|
||||
.thenThrow(new RuntimeException("Erreur de lecture des logs"));
|
||||
|
||||
given()
|
||||
.queryParam("level", "ERROR")
|
||||
.when()
|
||||
.get("/api/logs/export")
|
||||
.then()
|
||||
.statusCode(500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,4 +58,43 @@ class MembreDashboardMockResourceTest {
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Error cases
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/dashboard/membre/me sans authentification retourne 401")
|
||||
void getMonDashboard_sansAuthentification_returns401() {
|
||||
given()
|
||||
.when()
|
||||
.get("/api/dashboard/membre/me")
|
||||
.then()
|
||||
.statusCode(anyOf(equalTo(401), equalTo(403)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"})
|
||||
@DisplayName("GET /api/dashboard/membre/me quand service lève exception retourne 500")
|
||||
void getMonDashboard_serviceException_returns500() {
|
||||
when(dashboardService.getDashboardData())
|
||||
.thenThrow(new RuntimeException("Erreur interne dashboard"));
|
||||
|
||||
given()
|
||||
.when()
|
||||
.get("/api/dashboard/membre/me")
|
||||
.then()
|
||||
.statusCode(500);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
|
||||
@DisplayName("GET /api/dashboard/membre/me avec rôle ADMIN retourne 403")
|
||||
void getMonDashboard_avecRoleAdmin_returns403() {
|
||||
given()
|
||||
.when()
|
||||
.get("/api/dashboard/membre/me")
|
||||
.then()
|
||||
.statusCode(403);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
package dev.lions.unionflow.server.resource;
|
||||
|
||||
import dev.lions.unionflow.server.api.enums.membre.StatutMembre;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.MembreOrganisation;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
|
||||
import dev.lions.unionflow.server.service.MemberLifecycleService;
|
||||
import dev.lions.unionflow.server.service.MembreKeycloakSyncService;
|
||||
import dev.lions.unionflow.server.service.MembreService;
|
||||
import dev.lions.unionflow.server.service.MembreSuiviService;
|
||||
import dev.lions.unionflow.server.service.OrganisationService;
|
||||
import io.quarkus.test.InjectMock;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.quarkus.test.security.TestSecurity;
|
||||
import io.restassured.http.ContentType;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests RBAC pour les endpoints lifecycle du cycle de vie des membres.
|
||||
*
|
||||
* <p>Vérifie que :
|
||||
* <ul>
|
||||
* <li>MEMBRE ne peut pas activer/suspendre/radier (403)</li>
|
||||
* <li>ADMIN_ORGANISATION peut activer/suspendre/radier (200)</li>
|
||||
* <li>SUPER_ADMIN peut accéder à tous les endpoints admin (200)</li>
|
||||
* <li>accepter-invitation est PermitAll (accessible sans auth)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@QuarkusTest
|
||||
@DisplayName("MembreResource — RBAC lifecycle endpoints")
|
||||
class MembreResourceLifecycleRbacTest {
|
||||
|
||||
@InjectMock
|
||||
MembreService membreService;
|
||||
|
||||
@InjectMock
|
||||
MembreKeycloakSyncService keycloakSyncService;
|
||||
|
||||
@InjectMock
|
||||
MembreSuiviService membreSuiviService;
|
||||
|
||||
@InjectMock
|
||||
OrganisationService organisationService;
|
||||
|
||||
@InjectMock
|
||||
MemberLifecycleService memberLifecycleService;
|
||||
|
||||
@InjectMock
|
||||
MembreOrganisationRepository membreOrgRepository;
|
||||
|
||||
@InjectMock
|
||||
org.eclipse.microprofile.jwt.JsonWebToken jwt;
|
||||
|
||||
// ─── Helper ──────────────────────────────────────────────────────────────
|
||||
|
||||
private MembreOrganisation buildLien(UUID id, StatutMembre statut) {
|
||||
Membre membre = new Membre();
|
||||
membre.setId(UUID.randomUUID());
|
||||
membre.setNom("Test");
|
||||
membre.setPrenom("User");
|
||||
|
||||
Organisation org = new Organisation();
|
||||
org.setId(UUID.randomUUID());
|
||||
org.setNom("Org Test");
|
||||
|
||||
MembreOrganisation lien = MembreOrganisation.builder()
|
||||
.membre(membre)
|
||||
.organisation(org)
|
||||
.statutMembre(statut)
|
||||
.build();
|
||||
lien.setId(id);
|
||||
return lien;
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// activer-adhesion (PUT /{membreOrgId}/activer-adhesion)
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("PUT activer-adhesion: MEMBRE → 403 Forbidden")
|
||||
void activerAdhesion_membreRole_returns403() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(Map.of("motif", "test"))
|
||||
.when()
|
||||
.put("/api/membres/" + UUID.randomUUID() + "/activer-adhesion")
|
||||
.then()
|
||||
.statusCode(403);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("PUT activer-adhesion: ADMIN_ORGANISATION → 200")
|
||||
void activerAdhesion_adminOrganisation_returns200() {
|
||||
UUID membreOrgId = UUID.randomUUID();
|
||||
var lien = buildLien(membreOrgId, StatutMembre.EN_ATTENTE_VALIDATION);
|
||||
lien.setStatutMembre(StatutMembre.ACTIF); // Résultat après activation
|
||||
|
||||
when(memberLifecycleService.activerMembre(any(UUID.class), any(), any()))
|
||||
.thenReturn(lien);
|
||||
when(jwt.getSubject()).thenReturn(UUID.randomUUID().toString());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(Map.of("motif", "validation"))
|
||||
.when()
|
||||
.put("/api/membres/" + membreOrgId + "/activer-adhesion")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "superadmin@test.com", roles = {"SUPER_ADMIN"})
|
||||
@DisplayName("PUT activer-adhesion: SUPER_ADMIN → 200")
|
||||
void activerAdhesion_superAdmin_returns200() {
|
||||
UUID membreOrgId = UUID.randomUUID();
|
||||
var lien = buildLien(membreOrgId, StatutMembre.ACTIF);
|
||||
|
||||
when(memberLifecycleService.activerMembre(any(UUID.class), any(), any()))
|
||||
.thenReturn(lien);
|
||||
when(jwt.getSubject()).thenReturn(UUID.randomUUID().toString());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(Map.of())
|
||||
.when()
|
||||
.put("/api/membres/" + membreOrgId + "/activer-adhesion")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// suspendre-adhesion (PUT /{membreOrgId}/suspendre-adhesion)
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("PUT suspendre-adhesion: MEMBRE → 403 Forbidden")
|
||||
void suspendrAdhesion_membreRole_returns403() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(Map.of("motif", "test"))
|
||||
.when()
|
||||
.put("/api/membres/" + UUID.randomUUID() + "/suspendre-adhesion")
|
||||
.then()
|
||||
.statusCode(403);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("PUT suspendre-adhesion: ADMIN_ORGANISATION → 200")
|
||||
void suspendrAdhesion_adminOrganisation_returns200() {
|
||||
UUID membreOrgId = UUID.randomUUID();
|
||||
var lien = buildLien(membreOrgId, StatutMembre.SUSPENDU);
|
||||
|
||||
when(memberLifecycleService.suspendreMembre(any(UUID.class), any(), any()))
|
||||
.thenReturn(lien);
|
||||
when(jwt.getSubject()).thenReturn(UUID.randomUUID().toString());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(Map.of("motif", "manquements répétés"))
|
||||
.when()
|
||||
.put("/api/membres/" + membreOrgId + "/suspendre-adhesion")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// radier-adhesion (PUT /{membreOrgId}/radier-adhesion)
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("PUT radier-adhesion: MEMBRE → 403 Forbidden")
|
||||
void radierAdhesion_membreRole_returns403() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(Map.of())
|
||||
.when()
|
||||
.put("/api/membres/" + UUID.randomUUID() + "/radier-adhesion")
|
||||
.then()
|
||||
.statusCode(403);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||
@DisplayName("PUT radier-adhesion: ADMIN → 200")
|
||||
void radierAdhesion_admin_returns200() {
|
||||
UUID membreOrgId = UUID.randomUUID();
|
||||
var lien = buildLien(membreOrgId, StatutMembre.RADIE);
|
||||
|
||||
when(memberLifecycleService.radierMembre(any(UUID.class), any(), any()))
|
||||
.thenReturn(lien);
|
||||
when(jwt.getSubject()).thenReturn(UUID.randomUUID().toString());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(Map.of("motif", "exclusion définitive"))
|
||||
.when()
|
||||
.put("/api/membres/" + membreOrgId + "/radier-adhesion")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// inviter-organisation (PUT /{membreId}/inviter-organisation)
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "user@test.com", roles = {"USER"})
|
||||
@DisplayName("PUT inviter-organisation: USER → 403 Forbidden")
|
||||
void inviterOrganisation_userRole_returns403() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.queryParam("organisationId", UUID.randomUUID().toString())
|
||||
.when()
|
||||
.put("/api/membres/" + UUID.randomUUID() + "/inviter-organisation")
|
||||
.then()
|
||||
.statusCode(403);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("PUT inviter-organisation: ADMIN_ORGANISATION avec membre inexistant → 404")
|
||||
void inviterOrganisation_adminOrganisation_membreNotFound_returns404() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
UUID orgId = UUID.randomUUID();
|
||||
|
||||
when(membreService.trouverParId(membreId)).thenReturn(Optional.empty());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.queryParam("organisationId", orgId.toString())
|
||||
.when()
|
||||
.put("/api/membres/" + membreId + "/inviter-organisation")
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// accepter-invitation (POST /accepter-invitation/{token}) — PermitAll
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@DisplayName("POST accepter-invitation: sans authentification → PermitAll (token invalide → 400 ou 404)")
|
||||
void accepterInvitation_noAuth_permitAll() {
|
||||
// Pas d'auth — le endpoint est PermitAll
|
||||
when(memberLifecycleService.accepterInvitation(anyString()))
|
||||
.thenThrow(new IllegalArgumentException("Invitation introuvable ou déjà utilisée."));
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.when()
|
||||
.post("/api/membres/accepter-invitation/invalidtoken123")
|
||||
.then()
|
||||
.statusCode(400); // BadRequest pour IllegalArgumentException
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("POST accepter-invitation: MEMBRE avec token valide → 200")
|
||||
void accepterInvitation_membre_validToken_returns200() {
|
||||
String token = "validtoken1234567890abcdefghi12";
|
||||
var lien = buildLien(UUID.randomUUID(), StatutMembre.EN_ATTENTE_VALIDATION);
|
||||
|
||||
when(memberLifecycleService.accepterInvitation(token)).thenReturn(lien);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.when()
|
||||
.post("/api/membres/accepter-invitation/" + token)
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("statut", notNullValue());
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// adhesion statut (GET /{membreId}/adhesion) — lecture multi-rôle
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("GET /{membreId}/adhesion: MEMBRE peut consulter son statut (200 ou 404)")
|
||||
void getAdhesionStatut_membreRole_allowedToRead() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
UUID orgId = UUID.randomUUID();
|
||||
|
||||
when(membreOrgRepository.findByMembreIdAndOrganisationId(any(), any()))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
given()
|
||||
.queryParam("organisationId", orgId.toString())
|
||||
.when()
|
||||
.get("/api/membres/" + membreId + "/adhesion")
|
||||
.then()
|
||||
.statusCode(404); // Not found, mais pas 403
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("GET /{membreId}/adhesion: ADMIN_ORGANISATION peut consulter (200)")
|
||||
void getAdhesionStatut_adminOrganisation_returns200() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
UUID orgId = UUID.randomUUID();
|
||||
var lien = buildLien(UUID.randomUUID(), StatutMembre.ACTIF);
|
||||
|
||||
when(membreOrgRepository.findByMembreIdAndOrganisationId(any(), any()))
|
||||
.thenReturn(Optional.of(lien));
|
||||
|
||||
given()
|
||||
.queryParam("organisationId", orgId.toString())
|
||||
.when()
|
||||
.get("/api/membres/" + membreId + "/adhesion")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("statut", notNullValue());
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// archiver-adhesion (PUT /{membreOrgId}/archiver-adhesion)
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("PUT archiver-adhesion: MEMBRE → 403 Forbidden")
|
||||
void archiverAdhesion_membreRole_returns403() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(Map.of())
|
||||
.when()
|
||||
.put("/api/membres/" + UUID.randomUUID() + "/archiver-adhesion")
|
||||
.then()
|
||||
.statusCode(403);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("PUT archiver-adhesion: ADMIN_ORGANISATION → 200")
|
||||
void archiverAdhesion_adminOrganisation_returns200() {
|
||||
UUID membreOrgId = UUID.randomUUID();
|
||||
var lien = buildLien(membreOrgId, StatutMembre.ARCHIVE);
|
||||
|
||||
when(memberLifecycleService.archiverMembre(any(UUID.class), any()))
|
||||
.thenReturn(lien);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(Map.of("motif", "archivage annuel"))
|
||||
.when()
|
||||
.put("/api/membres/" + membreOrgId + "/archiver-adhesion")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
}
|
||||
@@ -303,4 +303,47 @@ class OrganisationResourceLambdaFilterTest {
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Error cases
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/organisations sans authentification retourne 401")
|
||||
void listerOrganisations_sansAuthentification_returns401() {
|
||||
given()
|
||||
.when()
|
||||
.get("/api/organisations")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "orgadmin@test.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("GET /api/organisations quand service lève exception retourne 500")
|
||||
void listerOrganisations_serviceException_returns500() {
|
||||
when(organisationService.listerOrganisationsPourUtilisateur(any()))
|
||||
.thenThrow(new RuntimeException("Erreur base de données"));
|
||||
|
||||
given()
|
||||
.when()
|
||||
.get("/api/organisations")
|
||||
.then()
|
||||
.statusCode(500);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "orgadmin@test.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("GET /api/organisations/{id} avec ID inexistant retourne 404")
|
||||
void obtenirOrganisation_notFound_returns404() {
|
||||
when(organisationService.trouverParId(any(UUID.class)))
|
||||
.thenReturn(java.util.Optional.empty());
|
||||
|
||||
given()
|
||||
.pathParam("id", UUID.randomUUID())
|
||||
.when()
|
||||
.get("/api/organisations/{id}")
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,4 +214,50 @@ class OrganisationResourceMissingBranchesTest {
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Error cases
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("listerMesOrganisations — service lève exception → exception propagée")
|
||||
void listerMesOrganisations_serviceException_propagee() {
|
||||
Principal principal = () -> "membre@test.com";
|
||||
when(securityIdentity.getPrincipal()).thenReturn(principal);
|
||||
when(organisationService.listerOrganisationsPourUtilisateur(anyString()))
|
||||
.thenThrow(new RuntimeException("Erreur base de données"));
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertThrows(RuntimeException.class,
|
||||
() -> organisationResource.listerMesOrganisations());
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "superadmin@test.com", roles = {"SUPER_ADMIN"})
|
||||
@DisplayName("listerOrganisations — SUPER_ADMIN + service retourne liste vide → résultat non null")
|
||||
void listerOrganisations_superAdmin_listeVide_retourneResultatNonNull() {
|
||||
when(securityIdentity.getRoles()).thenReturn(Set.of("SUPER_ADMIN"));
|
||||
when(organisationService.listerOrganisationsActives(anyInt(), anyInt())).thenReturn(List.of());
|
||||
when(organisationService.compterOrganisationsActives()).thenReturn(0L);
|
||||
|
||||
PagedResponse<OrganisationSummaryResponse> result = organisationResource.listerOrganisations(
|
||||
0, 20, null);
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getData()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "orgadmin@test.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("listerOrganisations — ADMIN_ORGANISATION + service lève exception → exception propagée")
|
||||
void listerOrganisations_adminOrg_serviceException_propagee() {
|
||||
when(securityIdentity.getRoles()).thenReturn(Set.of("ADMIN_ORGANISATION"));
|
||||
Principal principal = () -> "orgadmin@test.com";
|
||||
when(securityIdentity.getPrincipal()).thenReturn(principal);
|
||||
when(organisationService.listerOrganisationsPourUtilisateur(anyString()))
|
||||
.thenThrow(new RuntimeException("Accès base de données impossible"));
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertThrows(RuntimeException.class,
|
||||
() -> organisationResource.listerOrganisations(0, 20, null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,4 +63,46 @@ class PreferencesResourceTest {
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Error cases
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/preferences/{id} sans authentification retourne 401")
|
||||
void obtenirPreferences_sansAuthentification_returns401() {
|
||||
given()
|
||||
.pathParam("utilisateurId", UUID.randomUUID())
|
||||
.when()
|
||||
.get("/api/preferences/{utilisateurId}")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "membre@unionflow.com", roles = { "MEMBRE" })
|
||||
@DisplayName("PUT /api/preferences/{id} avec rôle MEMBRE retourne 403")
|
||||
void mettreAJourPreferences_avecRoleMembre_returns403() {
|
||||
given()
|
||||
.pathParam("utilisateurId", UUID.randomUUID())
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{}")
|
||||
.when()
|
||||
.put("/api/preferences/{utilisateurId}")
|
||||
.then()
|
||||
.statusCode(403);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PUT /api/preferences/{id} sans authentification retourne 401")
|
||||
void mettreAJourPreferences_sansAuthentification_returns401() {
|
||||
given()
|
||||
.pathParam("utilisateurId", UUID.randomUUID())
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{}")
|
||||
.when()
|
||||
.put("/api/preferences/{utilisateurId}")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,4 +150,64 @@ class PropositionAideMockResourceTest {
|
||||
.statusCode(200)
|
||||
.body("titre", equalTo("Aide trouvée"));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Error cases
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/propositions-aide sans authentification retourne 401")
|
||||
void listerToutes_sansAuthentification_returns401() {
|
||||
given()
|
||||
.when()
|
||||
.get("/api/propositions-aide")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /api/propositions-aide/{id} sans authentification retourne 401")
|
||||
void obtenirParId_sansAuthentification_returns401() {
|
||||
given()
|
||||
.pathParam("id", "id-quelconque")
|
||||
.when()
|
||||
.get("/api/propositions-aide/{id}")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
|
||||
@DisplayName("GET /api/propositions-aide/{id} — service retourne null → 404 (branche not found)")
|
||||
void obtenirParId_notFound_returns404() {
|
||||
when(propositionAideService.obtenirParId("id-inexistant")).thenReturn(null);
|
||||
|
||||
given()
|
||||
.pathParam("id", "id-inexistant")
|
||||
.when()
|
||||
.get("/api/propositions-aide/{id}")
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"})
|
||||
@DisplayName("PUT /api/propositions-aide/{id} avec rôle MEMBRE retourne 403")
|
||||
void mettreAJour_avecRoleMembre_returns403() {
|
||||
String body = """
|
||||
{
|
||||
"typeAide": "AIDE_FRAIS_MEDICAUX",
|
||||
"titre": "Aide non autorisée"
|
||||
}
|
||||
""";
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.pathParam("id", "prop-id-001")
|
||||
.body(body)
|
||||
.when()
|
||||
.put("/api/propositions-aide/{id}")
|
||||
.then()
|
||||
.statusCode(403);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,720 @@
|
||||
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.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.souscription.FormuleAbonnementResponse;
|
||||
import dev.lions.unionflow.server.api.dto.souscription.SouscriptionDemandeRequest;
|
||||
import dev.lions.unionflow.server.api.dto.souscription.SouscriptionStatutResponse;
|
||||
import dev.lions.unionflow.server.service.SouscriptionService;
|
||||
import dev.lions.unionflow.server.service.support.SecuriteHelper;
|
||||
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.BadRequestException;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests mock pour SouscriptionResource — couvre tous les endpoints.
|
||||
*
|
||||
* <p>Catégories testées :
|
||||
* <ul>
|
||||
* <li>Endpoints publics (PermitAll) : GET /formules, POST /confirmer-paiement</li>
|
||||
* <li>Endpoints @Authenticated : GET /ma-souscription, POST /demande, POST /{id}/initier-paiement</li>
|
||||
* <li>Endpoints SUPER_ADMIN : GET /admin/toutes, GET /admin/organisation/{id}/active,
|
||||
* GET /admin/en-attente, POST /admin/{id}/approuver, POST /admin/{id}/rejeter</li>
|
||||
* </ul>
|
||||
*/
|
||||
@QuarkusTest
|
||||
@DisplayName("SouscriptionResource (mock)")
|
||||
class SouscriptionResourceMockTest {
|
||||
|
||||
@InjectMock
|
||||
SouscriptionService souscriptionService;
|
||||
|
||||
@InjectMock
|
||||
SecuriteHelper securiteHelper;
|
||||
|
||||
// ── Helper ────────────────────────────────────────────────────────────────
|
||||
|
||||
private SouscriptionStatutResponse buildStatut(String statutValidation) {
|
||||
SouscriptionStatutResponse r = new SouscriptionStatutResponse();
|
||||
r.setSouscriptionId(UUID.randomUUID().toString());
|
||||
r.setStatutValidation(statutValidation);
|
||||
r.setStatutLibelle(statutValidation);
|
||||
r.setMontantTotal(new BigDecimal("10000"));
|
||||
r.setOrganisationId(UUID.randomUUID().toString());
|
||||
r.setOrganisationNom("Org Test");
|
||||
return r;
|
||||
}
|
||||
|
||||
private FormuleAbonnementResponse buildFormule() {
|
||||
FormuleAbonnementResponse f = new FormuleAbonnementResponse();
|
||||
f.setCode("BASIC");
|
||||
f.setLibelle("Formule Basic");
|
||||
f.setDescription("Formule de base");
|
||||
f.setPlage("PETITE");
|
||||
f.setPrixMensuel(new BigDecimal("3000"));
|
||||
return f;
|
||||
}
|
||||
|
||||
// ── GET /api/souscriptions/formules — PUBLIC ──────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /formules — retourne 200 avec la liste des formules (PermitAll)")
|
||||
void getFormules_success_returns200() {
|
||||
when(souscriptionService.getFormules()).thenReturn(List.of(buildFormule()));
|
||||
|
||||
given()
|
||||
.when()
|
||||
.get("/api/souscriptions/formules")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /formules — retourne 200 même avec liste vide (PermitAll)")
|
||||
void getFormules_listeVide_returns200() {
|
||||
when(souscriptionService.getFormules()).thenReturn(List.of());
|
||||
|
||||
given()
|
||||
.when()
|
||||
.get("/api/souscriptions/formules")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
// ── POST /api/souscriptions/confirmer-paiement — PUBLIC ──────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /confirmer-paiement — retourne 200 quand paiement confirmé avec succès")
|
||||
void confirmerPaiement_success_returns200() {
|
||||
UUID souscriptionId = UUID.randomUUID();
|
||||
doNothing().when(souscriptionService).confirmerPaiement(eq(souscriptionId), anyString());
|
||||
|
||||
given()
|
||||
.queryParam("id", souscriptionId.toString())
|
||||
.queryParam("wave_id", "WAVE-TXN-12345")
|
||||
.when()
|
||||
.post("/api/souscriptions/confirmer-paiement")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("message", equalTo("Paiement confirmé — compte activé"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /confirmer-paiement — retourne 200 sans wave_id (optionnel)")
|
||||
void confirmerPaiement_sansWaveId_returns200() {
|
||||
UUID souscriptionId = UUID.randomUUID();
|
||||
doNothing().when(souscriptionService).confirmerPaiement(eq(souscriptionId), isNull());
|
||||
|
||||
given()
|
||||
.queryParam("id", souscriptionId.toString())
|
||||
.when()
|
||||
.post("/api/souscriptions/confirmer-paiement")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /confirmer-paiement — retourne 400 si paramètre id manquant")
|
||||
void confirmerPaiement_idManquant_returns400() {
|
||||
given()
|
||||
.queryParam("wave_id", "WAVE-TXN-99")
|
||||
.when()
|
||||
.post("/api/souscriptions/confirmer-paiement")
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /confirmer-paiement — retourne 400 si service lève BadRequestException (statut invalide)")
|
||||
void confirmerPaiement_statutInvalide_returns400() {
|
||||
UUID souscriptionId = UUID.randomUUID();
|
||||
doThrow(new BadRequestException("Impossible de confirmer depuis le statut: REJETEE"))
|
||||
.when(souscriptionService).confirmerPaiement(eq(souscriptionId), anyString());
|
||||
|
||||
given()
|
||||
.queryParam("id", souscriptionId.toString())
|
||||
.queryParam("wave_id", "WAVE-TXN-BAD")
|
||||
.when()
|
||||
.post("/api/souscriptions/confirmer-paiement")
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /confirmer-paiement — retourne 404 si souscription introuvable")
|
||||
void confirmerPaiement_souscriptionInconnue_returns404() {
|
||||
UUID souscriptionId = UUID.randomUUID();
|
||||
doThrow(new NotFoundException("Souscription introuvable: " + souscriptionId))
|
||||
.when(souscriptionService).confirmerPaiement(eq(souscriptionId), anyString());
|
||||
|
||||
given()
|
||||
.queryParam("id", souscriptionId.toString())
|
||||
.queryParam("wave_id", "WAVE-TXN-INCO")
|
||||
.when()
|
||||
.post("/api/souscriptions/confirmer-paiement")
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
// ── GET /api/souscriptions/ma-souscription — @Authenticated ──────────────
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("GET /ma-souscription — retourne 200 quand souscription trouvée")
|
||||
void getMaSouscription_success_returns200() {
|
||||
SouscriptionStatutResponse statut = buildStatut("ACTIVE");
|
||||
when(souscriptionService.getMaSouscription()).thenReturn(statut);
|
||||
|
||||
given()
|
||||
.when()
|
||||
.get("/api/souscriptions/ma-souscription")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("GET /ma-souscription — retourne 404 si aucune souscription pour ce membre")
|
||||
void getMaSouscription_aucuneSouscription_returns404() {
|
||||
when(souscriptionService.getMaSouscription())
|
||||
.thenThrow(new NotFoundException("Aucune souscription trouvée pour ce membre"));
|
||||
|
||||
given()
|
||||
.when()
|
||||
.get("/api/souscriptions/ma-souscription")
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /ma-souscription — retourne 401 sans authentification")
|
||||
void getMaSouscription_nonAuthentifie_returns401() {
|
||||
given()
|
||||
.when()
|
||||
.get("/api/souscriptions/ma-souscription")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
|
||||
// ── POST /api/souscriptions/demande — @Authenticated ─────────────────────
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("POST /demande — retourne 201 quand demande créée avec succès")
|
||||
void creerDemande_success_returns201() {
|
||||
SouscriptionStatutResponse statut = buildStatut("EN_ATTENTE_PAIEMENT");
|
||||
when(souscriptionService.creerDemande(any(SouscriptionDemandeRequest.class))).thenReturn(statut);
|
||||
|
||||
String body = """
|
||||
{
|
||||
"typeFormule": "BASIC",
|
||||
"plageMembres": "PETITE",
|
||||
"typePeriode": "MENSUEL",
|
||||
"typeOrganisation": "ASSOCIATION",
|
||||
"organisationId": "%s"
|
||||
}
|
||||
""".formatted(UUID.randomUUID());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post("/api/souscriptions/demande")
|
||||
.then()
|
||||
.statusCode(201);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("POST /demande — retourne 400 si organisation introuvable")
|
||||
void creerDemande_orgInconnue_returns400() {
|
||||
when(souscriptionService.creerDemande(any(SouscriptionDemandeRequest.class)))
|
||||
.thenThrow(new NotFoundException("Organisation introuvable"));
|
||||
|
||||
String body = """
|
||||
{
|
||||
"typeFormule": "BASIC",
|
||||
"plageMembres": "PETITE",
|
||||
"typePeriode": "MENSUEL",
|
||||
"organisationId": "%s"
|
||||
}
|
||||
""".formatted(UUID.randomUUID());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post("/api/souscriptions/demande")
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("POST /demande — retourne 400 si souscription déjà existante pour l'organisation")
|
||||
void creerDemande_souscriptionExistante_returns400() {
|
||||
when(souscriptionService.creerDemande(any(SouscriptionDemandeRequest.class)))
|
||||
.thenThrow(new BadRequestException("Une souscription en cours existe déjà pour cette organisation"));
|
||||
|
||||
String body = """
|
||||
{
|
||||
"typeFormule": "BASIC",
|
||||
"plageMembres": "PETITE",
|
||||
"typePeriode": "MENSUEL",
|
||||
"organisationId": "%s"
|
||||
}
|
||||
""".formatted(UUID.randomUUID());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post("/api/souscriptions/demande")
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /demande — retourne 401 sans authentification")
|
||||
void creerDemande_nonAuthentifie_returns401() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{}")
|
||||
.when()
|
||||
.post("/api/souscriptions/demande")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
|
||||
// ── POST /api/souscriptions/{id}/initier-paiement — @Authenticated ───────
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("POST /{id}/initier-paiement — retourne 200 avec waveLaunchUrl quand session créée")
|
||||
void initierPaiement_success_returns200() {
|
||||
UUID souscriptionId = UUID.randomUUID();
|
||||
SouscriptionStatutResponse statut = buildStatut("PAIEMENT_INITIE");
|
||||
statut.setWaveLaunchUrl("https://pay.wave.com/checkout/abc123");
|
||||
when(souscriptionService.initierPaiementWave(souscriptionId)).thenReturn(statut);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.pathParam("id", souscriptionId.toString())
|
||||
.when()
|
||||
.post("/api/souscriptions/{id}/initier-paiement")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("POST /{id}/initier-paiement — retourne 404 si souscription introuvable")
|
||||
void initierPaiement_souscriptionInconnue_returns404() {
|
||||
UUID souscriptionId = UUID.randomUUID();
|
||||
when(souscriptionService.initierPaiementWave(souscriptionId))
|
||||
.thenThrow(new NotFoundException("Souscription introuvable: " + souscriptionId));
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.pathParam("id", souscriptionId.toString())
|
||||
.when()
|
||||
.post("/api/souscriptions/{id}/initier-paiement")
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("POST /{id}/initier-paiement — retourne 400 si statut invalide pour paiement")
|
||||
void initierPaiement_statutInvalide_returns400() {
|
||||
UUID souscriptionId = UUID.randomUUID();
|
||||
when(souscriptionService.initierPaiementWave(souscriptionId))
|
||||
.thenThrow(new BadRequestException("Impossible d'initier le paiement depuis le statut: VALIDEE"));
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.pathParam("id", souscriptionId.toString())
|
||||
.when()
|
||||
.post("/api/souscriptions/{id}/initier-paiement")
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /{id}/initier-paiement — retourne 401 sans authentification")
|
||||
void initierPaiement_nonAuthentifie_returns401() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.pathParam("id", UUID.randomUUID().toString())
|
||||
.when()
|
||||
.post("/api/souscriptions/{id}/initier-paiement")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
|
||||
// ── GET /api/souscriptions/admin/toutes — SUPER_ADMIN ────────────────────
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "superadmin@unionflow.com", roles = {"SUPER_ADMIN"})
|
||||
@DisplayName("GET /admin/toutes — retourne 200 avec liste complète")
|
||||
void getSouscriptionsToutes_success_returns200() {
|
||||
when(souscriptionService.listerToutes(isNull(), eq(0), eq(1000)))
|
||||
.thenReturn(List.of(buildStatut("ACTIVE"), buildStatut("EN_ATTENTE_PAIEMENT")));
|
||||
|
||||
given()
|
||||
.when()
|
||||
.get("/api/souscriptions/admin/toutes")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "superadmin@unionflow.com", roles = {"SUPER_ADMIN"})
|
||||
@DisplayName("GET /admin/toutes — retourne 200 avec filtre organisationId")
|
||||
void getSouscriptionsToutes_avecFiltreOrg_returns200() {
|
||||
UUID orgId = UUID.randomUUID();
|
||||
when(souscriptionService.listerToutes(eq(orgId), anyInt(), anyInt()))
|
||||
.thenReturn(List.of(buildStatut("ACTIVE")));
|
||||
|
||||
given()
|
||||
.queryParam("organisationId", orgId.toString())
|
||||
.when()
|
||||
.get("/api/souscriptions/admin/toutes")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "superadmin@unionflow.com", roles = {"SUPER_ADMIN"})
|
||||
@DisplayName("GET /admin/toutes — retourne 200 avec liste vide")
|
||||
void getSouscriptionsToutes_listeVide_returns200() {
|
||||
when(souscriptionService.listerToutes(isNull(), anyInt(), anyInt()))
|
||||
.thenReturn(List.of());
|
||||
|
||||
given()
|
||||
.when()
|
||||
.get("/api/souscriptions/admin/toutes")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("GET /admin/toutes — retourne 403 si rôle insuffisant (ADMIN_ORGANISATION)")
|
||||
void getSouscriptionsToutes_roleInsuffisant_returns403() {
|
||||
given()
|
||||
.when()
|
||||
.get("/api/souscriptions/admin/toutes")
|
||||
.then()
|
||||
.statusCode(403);
|
||||
}
|
||||
|
||||
// ── GET /api/souscriptions/admin/organisation/{id}/active — SUPER_ADMIN ──
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "superadmin@unionflow.com", roles = {"SUPER_ADMIN"})
|
||||
@DisplayName("GET /admin/organisation/{orgId}/active — retourne 200 quand souscription active trouvée")
|
||||
void getActiveParOrganisation_success_returns200() {
|
||||
UUID orgId = UUID.randomUUID();
|
||||
when(souscriptionService.obtenirActiveParOrganisation(orgId))
|
||||
.thenReturn(buildStatut("ACTIVE"));
|
||||
|
||||
given()
|
||||
.pathParam("organisationId", orgId.toString())
|
||||
.when()
|
||||
.get("/api/souscriptions/admin/organisation/{organisationId}/active")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "superadmin@unionflow.com", roles = {"SUPER_ADMIN"})
|
||||
@DisplayName("GET /admin/organisation/{orgId}/active — retourne 404 si aucune souscription active")
|
||||
void getActiveParOrganisation_aucuneActive_returns404() {
|
||||
UUID orgId = UUID.randomUUID();
|
||||
when(souscriptionService.obtenirActiveParOrganisation(orgId)).thenReturn(null);
|
||||
|
||||
given()
|
||||
.pathParam("organisationId", orgId.toString())
|
||||
.when()
|
||||
.get("/api/souscriptions/admin/organisation/{organisationId}/active")
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("GET /admin/organisation/{orgId}/active — retourne 403 si rôle insuffisant")
|
||||
void getActiveParOrganisation_roleInsuffisant_returns403() {
|
||||
given()
|
||||
.pathParam("organisationId", UUID.randomUUID().toString())
|
||||
.when()
|
||||
.get("/api/souscriptions/admin/organisation/{organisationId}/active")
|
||||
.then()
|
||||
.statusCode(403);
|
||||
}
|
||||
|
||||
// ── GET /api/souscriptions/admin/en-attente — SUPER_ADMIN ────────────────
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "superadmin@unionflow.com", roles = {"SUPER_ADMIN"})
|
||||
@DisplayName("GET /admin/en-attente — retourne 200 avec liste des souscriptions en attente")
|
||||
void getSouscriptionsEnAttente_success_returns200() {
|
||||
when(souscriptionService.getSouscriptionsEnAttenteValidation())
|
||||
.thenReturn(List.of(buildStatut("PAIEMENT_CONFIRME")));
|
||||
|
||||
given()
|
||||
.when()
|
||||
.get("/api/souscriptions/admin/en-attente")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "superadmin@unionflow.com", roles = {"SUPER_ADMIN"})
|
||||
@DisplayName("GET /admin/en-attente — retourne 200 avec liste vide si aucune en attente")
|
||||
void getSouscriptionsEnAttente_listeVide_returns200() {
|
||||
when(souscriptionService.getSouscriptionsEnAttenteValidation()).thenReturn(List.of());
|
||||
|
||||
given()
|
||||
.when()
|
||||
.get("/api/souscriptions/admin/en-attente")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("GET /admin/en-attente — retourne 403 si rôle insuffisant (ADMIN_ORGANISATION)")
|
||||
void getSouscriptionsEnAttente_roleInsuffisant_returns403() {
|
||||
given()
|
||||
.when()
|
||||
.get("/api/souscriptions/admin/en-attente")
|
||||
.then()
|
||||
.statusCode(403);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GET /admin/en-attente — retourne 401 sans authentification")
|
||||
void getSouscriptionsEnAttente_nonAuthentifie_returns401() {
|
||||
given()
|
||||
.when()
|
||||
.get("/api/souscriptions/admin/en-attente")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
|
||||
// ── POST /api/souscriptions/admin/{id}/approuver — SUPER_ADMIN ───────────
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "superadmin@unionflow.com", roles = {"SUPER_ADMIN"})
|
||||
@DisplayName("POST /admin/{id}/approuver — retourne 200 quand approbation réussie")
|
||||
void approuver_success_returns200() {
|
||||
UUID souscriptionId = UUID.randomUUID();
|
||||
UUID superAdminId = UUID.randomUUID();
|
||||
when(securiteHelper.resolveMembreId()).thenReturn(superAdminId);
|
||||
doNothing().when(souscriptionService).approuver(souscriptionId, superAdminId);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.pathParam("id", souscriptionId.toString())
|
||||
.when()
|
||||
.post("/api/souscriptions/admin/{id}/approuver")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("message", equalTo("Souscription approuvée — compte activé"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "superadmin@unionflow.com", roles = {"SUPER_ADMIN"})
|
||||
@DisplayName("POST /admin/{id}/approuver — retourne 404 si souscription introuvable")
|
||||
void approuver_souscriptionInconnue_returns404() {
|
||||
UUID souscriptionId = UUID.randomUUID();
|
||||
UUID superAdminId = UUID.randomUUID();
|
||||
when(securiteHelper.resolveMembreId()).thenReturn(superAdminId);
|
||||
doThrow(new NotFoundException("Souscription introuvable: " + souscriptionId))
|
||||
.when(souscriptionService).approuver(souscriptionId, superAdminId);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.pathParam("id", souscriptionId.toString())
|
||||
.when()
|
||||
.post("/api/souscriptions/admin/{id}/approuver")
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "superadmin@unionflow.com", roles = {"SUPER_ADMIN"})
|
||||
@DisplayName("POST /admin/{id}/approuver — retourne 400 si statut non approuvable")
|
||||
void approuver_statutNonApprouvable_returns400() {
|
||||
UUID souscriptionId = UUID.randomUUID();
|
||||
UUID superAdminId = UUID.randomUUID();
|
||||
when(securiteHelper.resolveMembreId()).thenReturn(superAdminId);
|
||||
doThrow(new BadRequestException("Impossible d'approuver depuis le statut: EN_ATTENTE_PAIEMENT"))
|
||||
.when(souscriptionService).approuver(souscriptionId, superAdminId);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.pathParam("id", souscriptionId.toString())
|
||||
.when()
|
||||
.post("/api/souscriptions/admin/{id}/approuver")
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("POST /admin/{id}/approuver — retourne 403 si rôle insuffisant")
|
||||
void approuver_roleInsuffisant_returns403() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.pathParam("id", UUID.randomUUID().toString())
|
||||
.when()
|
||||
.post("/api/souscriptions/admin/{id}/approuver")
|
||||
.then()
|
||||
.statusCode(403);
|
||||
}
|
||||
|
||||
// ── POST /api/souscriptions/admin/{id}/rejeter — SUPER_ADMIN ─────────────
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "superadmin@unionflow.com", roles = {"SUPER_ADMIN"})
|
||||
@DisplayName("POST /admin/{id}/rejeter — retourne 200 quand rejet réussi avec commentaire")
|
||||
void rejeter_success_returns200() {
|
||||
UUID souscriptionId = UUID.randomUUID();
|
||||
UUID superAdminId = UUID.randomUUID();
|
||||
when(securiteHelper.resolveMembreId()).thenReturn(superAdminId);
|
||||
doNothing().when(souscriptionService).rejeter(eq(souscriptionId), eq(superAdminId), anyString());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.pathParam("id", souscriptionId.toString())
|
||||
.body(Map.of("commentaire", "Documents manquants — dossier incomplet"))
|
||||
.when()
|
||||
.post("/api/souscriptions/admin/{id}/rejeter")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("message", equalTo("Souscription rejetée"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "superadmin@unionflow.com", roles = {"SUPER_ADMIN"})
|
||||
@DisplayName("POST /admin/{id}/rejeter — retourne 400 si commentaire absent")
|
||||
void rejeter_sansCommentaire_returns400() {
|
||||
UUID souscriptionId = UUID.randomUUID();
|
||||
when(securiteHelper.resolveMembreId()).thenReturn(UUID.randomUUID());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.pathParam("id", souscriptionId.toString())
|
||||
.body(Map.of())
|
||||
.when()
|
||||
.post("/api/souscriptions/admin/{id}/rejeter")
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "superadmin@unionflow.com", roles = {"SUPER_ADMIN"})
|
||||
@DisplayName("POST /admin/{id}/rejeter — retourne 400 si commentaire vide (blank)")
|
||||
void rejeter_commentaireVide_returns400() {
|
||||
UUID souscriptionId = UUID.randomUUID();
|
||||
when(securiteHelper.resolveMembreId()).thenReturn(UUID.randomUUID());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.pathParam("id", souscriptionId.toString())
|
||||
.body(Map.of("commentaire", " "))
|
||||
.when()
|
||||
.post("/api/souscriptions/admin/{id}/rejeter")
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "superadmin@unionflow.com", roles = {"SUPER_ADMIN"})
|
||||
@DisplayName("POST /admin/{id}/rejeter — retourne 404 si souscription introuvable")
|
||||
void rejeter_souscriptionInconnue_returns404() {
|
||||
UUID souscriptionId = UUID.randomUUID();
|
||||
UUID superAdminId = UUID.randomUUID();
|
||||
when(securiteHelper.resolveMembreId()).thenReturn(superAdminId);
|
||||
doThrow(new NotFoundException("Souscription introuvable: " + souscriptionId))
|
||||
.when(souscriptionService).rejeter(eq(souscriptionId), eq(superAdminId), anyString());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.pathParam("id", souscriptionId.toString())
|
||||
.body(Map.of("commentaire", "Motif de rejet"))
|
||||
.when()
|
||||
.post("/api/souscriptions/admin/{id}/rejeter")
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "superadmin@unionflow.com", roles = {"SUPER_ADMIN"})
|
||||
@DisplayName("POST /admin/{id}/rejeter — retourne 400 si souscription déjà dans état terminal")
|
||||
void rejeter_etatTerminal_returns400() {
|
||||
UUID souscriptionId = UUID.randomUUID();
|
||||
UUID superAdminId = UUID.randomUUID();
|
||||
when(securiteHelper.resolveMembreId()).thenReturn(superAdminId);
|
||||
doThrow(new BadRequestException("La souscription est déjà dans un état terminal: REJETEE"))
|
||||
.when(souscriptionService).rejeter(eq(souscriptionId), eq(superAdminId), anyString());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.pathParam("id", souscriptionId.toString())
|
||||
.body(Map.of("commentaire", "Motif de rejet"))
|
||||
.when()
|
||||
.post("/api/souscriptions/admin/{id}/rejeter")
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("POST /admin/{id}/rejeter — retourne 403 si rôle insuffisant")
|
||||
void rejeter_roleInsuffisant_returns403() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.pathParam("id", UUID.randomUUID().toString())
|
||||
.body(Map.of("commentaire", "Rejet non autorisé"))
|
||||
.when()
|
||||
.post("/api/souscriptions/admin/{id}/rejeter")
|
||||
.then()
|
||||
.statusCode(403);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /admin/{id}/rejeter — retourne 401 sans authentification")
|
||||
void rejeter_nonAuthentifie_returns401() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.pathParam("id", UUID.randomUUID().toString())
|
||||
.body(Map.of("commentaire", "test"))
|
||||
.when()
|
||||
.post("/api/souscriptions/admin/{id}/rejeter")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
}
|
||||
@@ -118,6 +118,54 @@ class WaveRedirectResourceMockDisabledUnitTest {
|
||||
assertThat(location).isNotNull().contains("success").contains(ref);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Error cases / edge cases
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* success() avec ref null et mockEnabled=false : la condition mockEnabled && ref != null
|
||||
* est fausse → branche false, retourne 303 sans appel de service.
|
||||
*/
|
||||
@Test
|
||||
@DisplayName("success avec mockEnabled=false + ref null → 303 (branche mockEnabled false, ref null)")
|
||||
void success_mockDisabled_nullRef_returns303() {
|
||||
Response response = resource.success(null);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(303);
|
||||
String location = response.getHeaderString("Location");
|
||||
assertThat(location).isNotNull();
|
||||
assertThat(location).startsWith("unionflow://");
|
||||
}
|
||||
|
||||
/**
|
||||
* success() avec ref blank et mockEnabled=false : la condition mockEnabled && ref != null
|
||||
* est fausse (mockEnabled=false) → retourne 303.
|
||||
*/
|
||||
@Test
|
||||
@DisplayName("success avec mockEnabled=false + ref blank → 303 (branche mockEnabled false, ref blank)")
|
||||
void success_mockDisabled_blankRef_returns303() {
|
||||
Response response = resource.success(" ");
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(303);
|
||||
String location = response.getHeaderString("Location");
|
||||
assertThat(location).isNotNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* mockComplete() avec un ref long (UUID complet) : vérifie que la construction du deep link
|
||||
* ne tronque pas le ref et inclut bien sa valeur complète.
|
||||
*/
|
||||
@Test
|
||||
@DisplayName("mockComplete avec ref UUID complet → deep link contient le ref intégralement")
|
||||
void mockComplete_mockDisabled_fullUuidRef_deepLinkContainsRef() {
|
||||
String longRef = "12345678-1234-1234-1234-123456789012";
|
||||
Response response = resource.mockComplete(longRef);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(303);
|
||||
String location = response.getHeaderString("Location");
|
||||
assertThat(location).isNotNull().contains("error");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper
|
||||
// =========================================================================
|
||||
|
||||
@@ -180,4 +180,61 @@ class DemandeCreditMockResourceTest {
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Error cases
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /api/v1/mutuelle/credits sans authentification retourne 401")
|
||||
void soumettreDemande_sansAuthentification_returns401() {
|
||||
String body = """
|
||||
{
|
||||
"membreId": "%s",
|
||||
"typeCredit": "CONSOMMATION",
|
||||
"montantDemande": 25000,
|
||||
"dureeMois": 12,
|
||||
"justificationDetaillee": "Besoin urgent"
|
||||
}
|
||||
""".formatted(UUID.randomUUID());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post("/api/v1/mutuelle/credits")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"})
|
||||
@DisplayName("POST /{id}/approbation avec rôle MEMBRE retourne 403")
|
||||
void approuver_avecRoleMembre_returns403() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.pathParam("id", UUID.randomUUID())
|
||||
.queryParam("montant", "50000")
|
||||
.queryParam("duree", "12")
|
||||
.queryParam("taux", "5.0")
|
||||
.when()
|
||||
.post("/api/v1/mutuelle/credits/{id}/approbation")
|
||||
.then()
|
||||
.statusCode(403);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN", "ADMIN_ORGANISATION", "MUTUELLE_RESP"})
|
||||
@DisplayName("GET /{id} — service lève exception retourne 500")
|
||||
void getDemandeById_serviceException_returns500() {
|
||||
when(demandeCreditService.getDemandeById(any(UUID.class)))
|
||||
.thenThrow(new RuntimeException("Crédit introuvable"));
|
||||
|
||||
given()
|
||||
.pathParam("id", UUID.randomUUID())
|
||||
.when()
|
||||
.get("/api/v1/mutuelle/credits/{id}")
|
||||
.then()
|
||||
.statusCode(500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,4 +50,69 @@ class TransactionEpargneResourceMockTest {
|
||||
.then()
|
||||
.statusCode(201);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Error cases
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("POST /api/v1/epargne/transactions/transfert sans authentification retourne 401")
|
||||
void transferer_sansAuthentification_returns401() {
|
||||
String body = String.format("""
|
||||
{
|
||||
"compteId": "%s",
|
||||
"typeTransaction": "TRANSFERT_SORTANT",
|
||||
"montant": 5000,
|
||||
"compteDestinationId": "%s",
|
||||
"motif": "Transfert test"
|
||||
}
|
||||
""", UUID.randomUUID(), UUID.randomUUID());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post("/api/v1/epargne/transactions/transfert")
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
|
||||
@DisplayName("POST /api/v1/epargne/transactions/transfert quand service lève exception retourne 500")
|
||||
void transferer_serviceException_returns500() {
|
||||
when(transactionEpargneService.transferer(any()))
|
||||
.thenThrow(new RuntimeException("Solde insuffisant pour le transfert"));
|
||||
|
||||
String body = String.format("""
|
||||
{
|
||||
"compteId": "%s",
|
||||
"typeTransaction": "TRANSFERT_SORTANT",
|
||||
"montant": 5000,
|
||||
"compteDestinationId": "%s",
|
||||
"motif": "Transfert erreur"
|
||||
}
|
||||
""", UUID.randomUUID(), UUID.randomUUID());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post("/api/v1/epargne/transactions/transfert")
|
||||
.then()
|
||||
.statusCode(500);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
|
||||
@DisplayName("POST /api/v1/epargne/transactions/transfert avec corps vide retourne 400")
|
||||
void transferer_corpsVide_returns400() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{}")
|
||||
.when()
|
||||
.post("/api/v1/epargne/transactions/transfert")
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
package dev.lions.unionflow.server.security;
|
||||
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.MembreOrganisation;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import dev.lions.unionflow.server.repository.OrganisationRepository;
|
||||
import dev.lions.unionflow.server.api.enums.membre.StatutMembre;
|
||||
import io.quarkus.test.InjectMock;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.quarkus.test.security.TestSecurity;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests d'intégration pour {@link OrganisationContextFilter}.
|
||||
*
|
||||
* <p>Vérifie le comportement du filtre multi-org selon le header
|
||||
* {@code X-Active-Organisation-Id} :
|
||||
* <ul>
|
||||
* <li>Absent → passe (pas de contexte org requis)</li>
|
||||
* <li>UUID invalide → 400 Bad Request</li>
|
||||
* <li>Organisation inexistante → 404 Not Found</li>
|
||||
* <li>Utilisateur non membre de l'org → 403 Forbidden</li>
|
||||
* <li>Membre actif → contexte résolu, requête passée</li>
|
||||
* <li>SUPER_ADMIN → toujours autorisé (bypass membre check)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Utilise l'endpoint {@code GET /api/v1/dashboard/health} (PermitAll-like)
|
||||
* comme cible neutre pour tester le filtre.
|
||||
*/
|
||||
@QuarkusTest
|
||||
@DisplayName("OrganisationContextFilter — multi-org header validation")
|
||||
class OrganisationContextFilterMultiOrgTest {
|
||||
|
||||
@InjectMock
|
||||
OrganisationRepository organisationRepository;
|
||||
|
||||
@InjectMock
|
||||
MembreRepository membreRepository;
|
||||
|
||||
@InjectMock
|
||||
MembreOrganisationRepository membreOrganisationRepository;
|
||||
|
||||
// ─── Aucun header ────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||
@DisplayName("Sans header X-Active-Organisation-Id : filtre passe, endpoint répond")
|
||||
void noHeader_filterPasses_dashboardReachable() {
|
||||
given()
|
||||
.when()
|
||||
.get("/api/v1/dashboard/health")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
// ─── UUID invalide ────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||
@DisplayName("Header avec UUID invalide → 400 Bad Request")
|
||||
void invalidUuidHeader_returns400() {
|
||||
given()
|
||||
.header("X-Active-Organisation-Id", "not-a-valid-uuid")
|
||||
.when()
|
||||
.get("/api/v1/dashboard/health")
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
// ─── Organisation introuvable ─────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN_ORGANISATION"})
|
||||
@DisplayName("Header avec UUID valide mais org inexistante → 404 Not Found")
|
||||
void validUuidButOrgNotFound_returns404() {
|
||||
UUID unknownOrgId = UUID.randomUUID();
|
||||
when(organisationRepository.findByIdOptional(unknownOrgId)).thenReturn(Optional.empty());
|
||||
|
||||
given()
|
||||
.header("X-Active-Organisation-Id", unknownOrgId.toString())
|
||||
.when()
|
||||
.get("/api/v1/dashboard/health")
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
// ─── Utilisateur non membre ───────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("Header valide + utilisateur non membre de l'org → 403 Forbidden")
|
||||
void memberNotInOrg_returns403() {
|
||||
UUID orgId = UUID.randomUUID();
|
||||
Organisation org = new Organisation();
|
||||
org.setId(orgId);
|
||||
org.setNom("Org Tierce");
|
||||
|
||||
Membre membre = new Membre();
|
||||
membre.setId(UUID.randomUUID());
|
||||
membre.setEmail("membre@test.com");
|
||||
|
||||
when(organisationRepository.findByIdOptional(orgId)).thenReturn(Optional.of(org));
|
||||
when(membreRepository.findByEmail("membre@test.com")).thenReturn(Optional.of(membre));
|
||||
when(membreOrganisationRepository.findByMembreIdAndOrganisationId(membre.getId(), orgId))
|
||||
.thenReturn(Optional.empty()); // Pas membre
|
||||
|
||||
given()
|
||||
.header("X-Active-Organisation-Id", orgId.toString())
|
||||
.when()
|
||||
.get("/api/v1/dashboard/health")
|
||||
.then()
|
||||
.statusCode(403);
|
||||
}
|
||||
|
||||
// ─── Membre non actif ─────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "suspendu@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("Header valide + membre suspendu dans l'org → 403 Forbidden")
|
||||
void memberSuspended_returns403() {
|
||||
UUID orgId = UUID.randomUUID();
|
||||
Organisation org = new Organisation();
|
||||
org.setId(orgId);
|
||||
org.setNom("Org Test");
|
||||
|
||||
Membre membre = new Membre();
|
||||
membre.setId(UUID.randomUUID());
|
||||
membre.setEmail("suspendu@test.com");
|
||||
|
||||
MembreOrganisation membreOrg = MembreOrganisation.builder()
|
||||
.membre(membre)
|
||||
.organisation(org)
|
||||
.statutMembre(StatutMembre.SUSPENDU)
|
||||
.build();
|
||||
membreOrg.setId(UUID.randomUUID());
|
||||
|
||||
when(organisationRepository.findByIdOptional(orgId)).thenReturn(Optional.of(org));
|
||||
when(membreRepository.findByEmail("suspendu@test.com")).thenReturn(Optional.of(membre));
|
||||
when(membreOrganisationRepository.findByMembreIdAndOrganisationId(membre.getId(), orgId))
|
||||
.thenReturn(Optional.of(membreOrg));
|
||||
|
||||
given()
|
||||
.header("X-Active-Organisation-Id", orgId.toString())
|
||||
.when()
|
||||
.get("/api/v1/dashboard/health")
|
||||
.then()
|
||||
.statusCode(403);
|
||||
}
|
||||
|
||||
// ─── SUPER_ADMIN bypass ───────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "superadmin@test.com", roles = {"SUPER_ADMIN", "ADMIN"})
|
||||
@DisplayName("SUPER_ADMIN avec header valide → bypass check membre, contexte résolu (200)")
|
||||
void superAdmin_validOrgId_bypassesMemberCheck() {
|
||||
UUID orgId = UUID.randomUUID();
|
||||
Organisation org = new Organisation();
|
||||
org.setId(orgId);
|
||||
org.setNom("Org Admin");
|
||||
|
||||
when(organisationRepository.findByIdOptional(orgId)).thenReturn(Optional.of(org));
|
||||
// Pas besoin de mocker membreRepository — SUPER_ADMIN bypass
|
||||
|
||||
given()
|
||||
.header("X-Active-Organisation-Id", orgId.toString())
|
||||
.when()
|
||||
.get("/api/v1/dashboard/health")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
// ─── Membre actif ─────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "actif@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("Header valide + membre ACTIF dans l'org → contexte résolu (200)")
|
||||
void activeMember_validOrgId_contextResolved() {
|
||||
UUID orgId = UUID.randomUUID();
|
||||
Organisation org = new Organisation();
|
||||
org.setId(orgId);
|
||||
org.setNom("Ma Tontine");
|
||||
|
||||
Membre membre = new Membre();
|
||||
membre.setId(UUID.randomUUID());
|
||||
membre.setEmail("actif@test.com");
|
||||
|
||||
MembreOrganisation membreOrg = MembreOrganisation.builder()
|
||||
.membre(membre)
|
||||
.organisation(org)
|
||||
.statutMembre(StatutMembre.ACTIF)
|
||||
.build();
|
||||
membreOrg.setId(UUID.randomUUID());
|
||||
|
||||
when(organisationRepository.findByIdOptional(orgId)).thenReturn(Optional.of(org));
|
||||
when(membreRepository.findByEmail("actif@test.com")).thenReturn(Optional.of(membre));
|
||||
when(membreOrganisationRepository.findByMembreIdAndOrganisationId(membre.getId(), orgId))
|
||||
.thenReturn(Optional.of(membreOrg));
|
||||
|
||||
given()
|
||||
.header("X-Active-Organisation-Id", orgId.toString())
|
||||
.when()
|
||||
.get("/api/v1/dashboard/health")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package dev.lions.unionflow.server.security;
|
||||
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour {@link OrganisationContextHolder}.
|
||||
*
|
||||
* <p>Couvre la logique de résolution du contexte multi-organisation.
|
||||
*/
|
||||
@DisplayName("OrganisationContextHolder — logique du contexte multi-org")
|
||||
class OrganisationContextHolderTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("hasContext: non résolu et orgId null → false")
|
||||
void hasContext_notResolvedAndNullOrgId_returnsFalse() {
|
||||
OrganisationContextHolder holder = new OrganisationContextHolder();
|
||||
assertThat(holder.hasContext()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("hasContext: résolu mais orgId null → false")
|
||||
void hasContext_resolvedButNullOrgId_returnsFalse() {
|
||||
OrganisationContextHolder holder = new OrganisationContextHolder();
|
||||
holder.setResolved(true);
|
||||
holder.setOrganisationId(null);
|
||||
|
||||
assertThat(holder.hasContext()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("hasContext: orgId défini mais non résolu → false")
|
||||
void hasContext_orgIdSetButNotResolved_returnsFalse() {
|
||||
OrganisationContextHolder holder = new OrganisationContextHolder();
|
||||
holder.setOrganisationId(UUID.randomUUID());
|
||||
holder.setResolved(false);
|
||||
|
||||
assertThat(holder.hasContext()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("hasContext: résolu ET orgId défini → true")
|
||||
void hasContext_resolvedAndOrgIdSet_returnsTrue() {
|
||||
OrganisationContextHolder holder = new OrganisationContextHolder();
|
||||
holder.setOrganisationId(UUID.randomUUID());
|
||||
holder.setResolved(true);
|
||||
|
||||
assertThat(holder.hasContext()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("setOrganisation: organisation correctement stockée")
|
||||
void setOrganisation_storesCorrectly() {
|
||||
OrganisationContextHolder holder = new OrganisationContextHolder();
|
||||
Organisation org = new Organisation();
|
||||
UUID orgId = UUID.randomUUID();
|
||||
org.setId(orgId);
|
||||
org.setNom("Tontine Dakar");
|
||||
|
||||
holder.setOrganisation(org);
|
||||
holder.setOrganisationId(orgId);
|
||||
holder.setResolved(true);
|
||||
|
||||
assertThat(holder.getOrganisation()).isSameAs(org);
|
||||
assertThat(holder.getOrganisationId()).isEqualTo(orgId);
|
||||
assertThat(holder.isResolved()).isTrue();
|
||||
assertThat(holder.hasContext()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("État initial: tous les champs sont null/false")
|
||||
void initialState_allFieldsAreDefault() {
|
||||
OrganisationContextHolder holder = new OrganisationContextHolder();
|
||||
|
||||
assertThat(holder.getOrganisationId()).isNull();
|
||||
assertThat(holder.getOrganisation()).isNull();
|
||||
assertThat(holder.isResolved()).isFalse();
|
||||
assertThat(holder.hasContext()).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationSummaryResponse;
|
||||
import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationResponse;
|
||||
import dev.lions.unionflow.server.entity.Cotisation;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
@@ -108,10 +108,10 @@ class CotisationServiceAdminBranchesTest {
|
||||
any(), any(Page.class), any(Sort.class)))
|
||||
.thenReturn(List.of(cotisation));
|
||||
|
||||
List<CotisationSummaryResponse> result = cotisationService.getMesCotisations(0, 10);
|
||||
List<CotisationResponse> result = cotisationService.getMesCotisations(0, 10);
|
||||
|
||||
assertThat(result).isNotNull().hasSize(1);
|
||||
assertThat(result.get(0).id()).isEqualTo(cotisation.getId());
|
||||
assertThat(result.get(0).getId()).isEqualTo(cotisation.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -122,7 +122,7 @@ class CotisationServiceAdminBranchesTest {
|
||||
when(organisationService.listerOrganisationsPourUtilisateur("admin@unionflow.com"))
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
List<CotisationSummaryResponse> result = cotisationService.getMesCotisations(0, 10);
|
||||
List<CotisationResponse> result = cotisationService.getMesCotisations(0, 10);
|
||||
|
||||
assertThat(result).isNotNull().isEmpty();
|
||||
}
|
||||
@@ -143,7 +143,7 @@ class CotisationServiceAdminBranchesTest {
|
||||
any(), any(Page.class), any(Sort.class)))
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
List<CotisationSummaryResponse> result = cotisationService.getMesCotisations(0, 10);
|
||||
List<CotisationResponse> result = cotisationService.getMesCotisations(0, 10);
|
||||
|
||||
assertThat(result).isNotNull().isEmpty();
|
||||
}
|
||||
@@ -156,7 +156,7 @@ class CotisationServiceAdminBranchesTest {
|
||||
when(organisationService.listerOrganisationsPourUtilisateur("admin@unionflow.com"))
|
||||
.thenReturn(null);
|
||||
|
||||
List<CotisationSummaryResponse> result = cotisationService.getMesCotisations(0, 10);
|
||||
List<CotisationResponse> result = cotisationService.getMesCotisations(0, 10);
|
||||
|
||||
assertThat(result).isNotNull().isEmpty();
|
||||
}
|
||||
@@ -166,7 +166,7 @@ class CotisationServiceAdminBranchesTest {
|
||||
void getMesCotisations_emailNull_retourneListeVide() {
|
||||
when(securiteHelper.resolveEmail()).thenReturn(null);
|
||||
|
||||
List<CotisationSummaryResponse> result = cotisationService.getMesCotisations(0, 10);
|
||||
List<CotisationResponse> result = cotisationService.getMesCotisations(0, 10);
|
||||
|
||||
assertThat(result).isNotNull().isEmpty();
|
||||
}
|
||||
@@ -176,7 +176,7 @@ class CotisationServiceAdminBranchesTest {
|
||||
void getMesCotisations_emailBlank_retourneListeVide() {
|
||||
when(securiteHelper.resolveEmail()).thenReturn(" ");
|
||||
|
||||
List<CotisationSummaryResponse> result = cotisationService.getMesCotisations(0, 10);
|
||||
List<CotisationResponse> result = cotisationService.getMesCotisations(0, 10);
|
||||
|
||||
assertThat(result).isNotNull().isEmpty();
|
||||
}
|
||||
@@ -189,7 +189,7 @@ class CotisationServiceAdminBranchesTest {
|
||||
when(membreRepository.findByEmail("membre-inconnu@unionflow.com"))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
List<CotisationSummaryResponse> result = cotisationService.getMesCotisations(0, 10);
|
||||
List<CotisationResponse> result = cotisationService.getMesCotisations(0, 10);
|
||||
|
||||
assertThat(result).isNotNull().isEmpty();
|
||||
}
|
||||
@@ -228,10 +228,10 @@ class CotisationServiceAdminBranchesTest {
|
||||
when(cotisationRepository.findEnAttenteByOrganisationIdIn(any()))
|
||||
.thenReturn(List.of(cotisation));
|
||||
|
||||
List<CotisationSummaryResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
List<CotisationResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
|
||||
assertThat(result).isNotNull().hasSize(1);
|
||||
assertThat(result.get(0).statut()).isEqualTo("EN_ATTENTE");
|
||||
assertThat(result.get(0).getStatut()).isEqualTo("EN_ATTENTE");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -259,10 +259,10 @@ class CotisationServiceAdminBranchesTest {
|
||||
when(cotisationRepository.findEnAttenteByOrganisationIdIn(any()))
|
||||
.thenReturn(List.of(cotisation));
|
||||
|
||||
List<CotisationSummaryResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
List<CotisationResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
|
||||
assertThat(result).isNotNull().hasSize(1);
|
||||
assertThat(result.get(0).statut()).isEqualTo("EN_ATTENTE");
|
||||
assertThat(result.get(0).getStatut()).isEqualTo("EN_ATTENTE");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -273,7 +273,7 @@ class CotisationServiceAdminBranchesTest {
|
||||
when(organisationService.listerOrganisationsPourUtilisateur("admin-vide@unionflow.com"))
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
List<CotisationSummaryResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
List<CotisationResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
|
||||
assertThat(result).isNotNull().isEmpty();
|
||||
}
|
||||
@@ -283,7 +283,7 @@ class CotisationServiceAdminBranchesTest {
|
||||
void getMesCotisationsEnAttente_emailNull_retourneListeVide() {
|
||||
when(securiteHelper.resolveEmail()).thenReturn(null);
|
||||
|
||||
List<CotisationSummaryResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
List<CotisationResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
|
||||
assertThat(result).isNotNull().isEmpty();
|
||||
}
|
||||
@@ -295,7 +295,7 @@ class CotisationServiceAdminBranchesTest {
|
||||
when(securiteHelper.getRoles()).thenReturn(Set.of("MEMBRE"));
|
||||
when(membreRepository.findByEmail("inconnu@unionflow.com")).thenReturn(Optional.empty());
|
||||
|
||||
List<CotisationSummaryResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
List<CotisationResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
|
||||
assertThat(result).isNotNull().isEmpty();
|
||||
}
|
||||
@@ -308,7 +308,7 @@ class CotisationServiceAdminBranchesTest {
|
||||
when(securiteHelper.getRoles()).thenReturn(null);
|
||||
when(membreRepository.findByEmail("roles.null@unionflow.com")).thenReturn(Optional.empty());
|
||||
|
||||
List<CotisationSummaryResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
List<CotisationResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
|
||||
assertThat(result).isNotNull().isEmpty();
|
||||
}
|
||||
@@ -318,7 +318,7 @@ class CotisationServiceAdminBranchesTest {
|
||||
void getMesCotisationsEnAttente_emailBlank_retourneListeVide() {
|
||||
when(securiteHelper.resolveEmail()).thenReturn(" "); // blank → L736 true
|
||||
|
||||
List<CotisationSummaryResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
List<CotisationResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
|
||||
assertThat(result).isNotNull().isEmpty();
|
||||
}
|
||||
@@ -499,7 +499,7 @@ class CotisationServiceAdminBranchesTest {
|
||||
when(organisationService.listerOrganisationsPourUtilisateur("admin-null-ea@unionflow.com"))
|
||||
.thenReturn(null); // null → orgs == null → true → return empty
|
||||
|
||||
List<CotisationSummaryResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
List<CotisationResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
|
||||
assertThat(result).isNotNull().isEmpty();
|
||||
}
|
||||
@@ -570,7 +570,7 @@ class CotisationServiceAdminBranchesTest {
|
||||
org.mockito.ArgumentMatchers.any(Sort.class)))
|
||||
.thenReturn(java.util.Collections.emptyList());
|
||||
|
||||
List<CotisationSummaryResponse> result = cotisationService.getMesCotisations(0, 10);
|
||||
List<CotisationResponse> result = cotisationService.getMesCotisations(0, 10);
|
||||
assertThat(result).isNotNull();
|
||||
}
|
||||
|
||||
@@ -595,7 +595,7 @@ class CotisationServiceAdminBranchesTest {
|
||||
when(q.setParameter(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.any())).thenReturn(q);
|
||||
when(q.getResultList()).thenReturn(java.util.Collections.emptyList());
|
||||
|
||||
List<CotisationSummaryResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
List<CotisationResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
assertThat(result).isNotNull();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationResponse;
|
||||
import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationSummaryResponse;
|
||||
import dev.lions.unionflow.server.entity.Cotisation;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
@@ -288,7 +287,7 @@ class CotisationServiceFinalBranchesTest {
|
||||
.thenReturn(cotQuery);
|
||||
when(cotQuery.getResultList()).thenReturn(Collections.emptyList());
|
||||
|
||||
List<CotisationSummaryResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
List<CotisationResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
|
||||
assertThat(result).isNotNull().isEmpty();
|
||||
}
|
||||
@@ -329,7 +328,7 @@ class CotisationServiceFinalBranchesTest {
|
||||
cot.setMembre(membre);
|
||||
when(cotQuery.getResultList()).thenReturn(List.of(cot));
|
||||
|
||||
List<CotisationSummaryResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
List<CotisationResponse> result = cotisationService.getMesCotisationsEnAttente();
|
||||
|
||||
assertThat(result).isNotNull().hasSize(1);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.cotisation.request.CreateCotisationRequest;
|
||||
import dev.lions.unionflow.server.api.dto.cotisation.request.UpdateCotisationRequest;
|
||||
import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationSummaryResponse;
|
||||
import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationResponse;
|
||||
import dev.lions.unionflow.server.entity.Cotisation;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
@@ -311,7 +311,7 @@ class CotisationServiceTest {
|
||||
void getCotisationsByMembre_existant_returnsList() {
|
||||
var list = cotisationService.getCotisationsByMembre(membre.getId(), 0, 10);
|
||||
assertThat(list).isNotEmpty();
|
||||
assertThat(list.get(0).id()).isEqualTo(cotisation.getId());
|
||||
assertThat(list.get(0).getId()).isEqualTo(cotisation.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -352,12 +352,12 @@ class CotisationServiceTest {
|
||||
@TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"})
|
||||
@DisplayName("getMesCotisationsEnAttente → retourne seulement les cotisations EN_ATTENTE du membre connecté")
|
||||
void getMesCotisationsEnAttente_returnsOnlyMemberCotisations() {
|
||||
List<CotisationSummaryResponse> results = cotisationService.getMesCotisationsEnAttente();
|
||||
List<CotisationResponse> results = cotisationService.getMesCotisationsEnAttente();
|
||||
|
||||
assertThat(results).isNotNull();
|
||||
assertThat(results).isNotEmpty();
|
||||
assertThat(results).allMatch(c -> c.statut().equals("EN_ATTENTE"));
|
||||
assertThat(results.get(0).id()).isEqualTo(cotisation.getId());
|
||||
assertThat(results).allMatch(c -> c.getStatut().equals("EN_ATTENTE"));
|
||||
assertThat(results.get(0).getId()).isEqualTo(cotisation.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -382,12 +382,12 @@ class CotisationServiceTest {
|
||||
cotisationNextYear.setNumeroReference("COT-TEST-NY-" + java.util.UUID.randomUUID().toString().substring(0, 8));
|
||||
cotisationRepository.persist(cotisationNextYear);
|
||||
|
||||
List<CotisationSummaryResponse> results = cotisationService.getMesCotisationsEnAttente();
|
||||
List<CotisationResponse> results = cotisationService.getMesCotisationsEnAttente();
|
||||
|
||||
// Ne doit retourner que la cotisation de l'année en cours
|
||||
assertThat(results).isNotNull();
|
||||
assertThat(results).allMatch(c ->
|
||||
c.dateEcheance().getYear() == LocalDate.now().getYear()
|
||||
c.getDateEcheance().getYear() == LocalDate.now().getYear()
|
||||
);
|
||||
|
||||
// Cleanup
|
||||
@@ -709,9 +709,9 @@ class CotisationServiceTest {
|
||||
@DisplayName("getMesCotisations comme MEMBRE → retourne les cotisations du membre connecté")
|
||||
void getMesCotisations_commeMembreConnecte_retourneSesCotsisations() {
|
||||
// Couvre la branche MEMBRE : membreConnecte != null → getCotisationsByMembre(...)
|
||||
List<CotisationSummaryResponse> result = cotisationService.getMesCotisations(0, 10);
|
||||
List<CotisationResponse> result = cotisationService.getMesCotisations(0, 10);
|
||||
assertThat(result).isNotNull().isNotEmpty();
|
||||
assertThat(result.stream().anyMatch(c -> c.id().equals(cotisation.getId()))).isTrue();
|
||||
assertThat(result.stream().anyMatch(c -> c.getId().equals(cotisation.getId()))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.enums.membre.StatutMembre;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.MembreOrganisation;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheQuery;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
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.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour {@link MemberLifecycleService}.
|
||||
*
|
||||
* <p>Couvre les transitions de statut manuel (activer, suspendre, radier, archiver, inviter)
|
||||
* ainsi que les traitements automatiques (expiration des invitations, rappels).
|
||||
*/
|
||||
@DisplayName("MemberLifecycleService — transitions de statut")
|
||||
class MemberLifecycleServiceTest {
|
||||
|
||||
@InjectMocks
|
||||
MemberLifecycleService service;
|
||||
|
||||
@Mock
|
||||
MembreOrganisationRepository membreOrgRepository;
|
||||
|
||||
@Mock
|
||||
NotificationService notificationService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
}
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
private MembreOrganisation buildLien(StatutMembre statut) {
|
||||
Membre membre = new Membre();
|
||||
membre.setId(UUID.randomUUID());
|
||||
membre.setNom("Dupont");
|
||||
membre.setPrenom("Marie");
|
||||
|
||||
Organisation org = new Organisation();
|
||||
org.setId(UUID.randomUUID());
|
||||
org.setNom("Association Test");
|
||||
|
||||
MembreOrganisation lien = MembreOrganisation.builder()
|
||||
.membre(membre)
|
||||
.organisation(org)
|
||||
.statutMembre(statut)
|
||||
.build();
|
||||
lien.setId(UUID.randomUUID());
|
||||
return lien;
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// activerMembre
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@DisplayName("activerMembre: EN_ATTENTE_VALIDATION → ACTIF")
|
||||
void activerMembre_enAttenteValidation_becomesActif() {
|
||||
var lien = buildLien(StatutMembre.EN_ATTENTE_VALIDATION);
|
||||
when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
|
||||
doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));
|
||||
|
||||
var result = service.activerMembre(lien.getId(), UUID.randomUUID(), "validation admin");
|
||||
|
||||
assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.ACTIF);
|
||||
assertThat(result.getMotifStatut()).isEqualTo("validation admin");
|
||||
verify(membreOrgRepository).persist(lien);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("activerMembre: INVITE → ACTIF (validation directe)")
|
||||
void activerMembre_invite_becomesActif() {
|
||||
var lien = buildLien(StatutMembre.INVITE);
|
||||
when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
|
||||
doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));
|
||||
|
||||
var result = service.activerMembre(lien.getId(), UUID.randomUUID(), null);
|
||||
|
||||
assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.ACTIF);
|
||||
assertThat(result.getMotifStatut()).isEqualTo("Validation par l'administrateur");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("activerMembre: SUSPENDU → ACTIF (réactivation)")
|
||||
void activerMembre_suspendu_becomesActif() {
|
||||
var lien = buildLien(StatutMembre.SUSPENDU);
|
||||
when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
|
||||
doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));
|
||||
|
||||
var result = service.activerMembre(lien.getId(), UUID.randomUUID(), "levée suspension");
|
||||
|
||||
assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.ACTIF);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("activerMembre: RADIE → IllegalStateException (transition non autorisée)")
|
||||
void activerMembre_radie_throwsIllegalState() {
|
||||
var lien = buildLien(StatutMembre.RADIE);
|
||||
when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
|
||||
|
||||
assertThatThrownBy(() -> service.activerMembre(lien.getId(), UUID.randomUUID(), null))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("Transition non autorisée");
|
||||
|
||||
verify(membreOrgRepository, never()).persist(any(MembreOrganisation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("activerMembre: ARCHIVE → IllegalStateException")
|
||||
void activerMembre_archive_throwsIllegalState() {
|
||||
var lien = buildLien(StatutMembre.ARCHIVE);
|
||||
when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
|
||||
|
||||
assertThatThrownBy(() -> service.activerMembre(lien.getId(), UUID.randomUUID(), null))
|
||||
.isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("activerMembre: ID inexistant → IllegalArgumentException")
|
||||
void activerMembre_unknownId_throwsIllegalArgument() {
|
||||
UUID unknown = UUID.randomUUID();
|
||||
when(membreOrgRepository.findByIdOptional(unknown)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> service.activerMembre(unknown, UUID.randomUUID(), null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("introuvable");
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// suspendreMembre
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@DisplayName("suspendreMembre: ACTIF → SUSPENDU avec motif")
|
||||
void suspendreMembre_actif_becomesSuspenduWithMotif() {
|
||||
var lien = buildLien(StatutMembre.ACTIF);
|
||||
when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
|
||||
doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));
|
||||
|
||||
var result = service.suspendreMembre(lien.getId(), UUID.randomUUID(), "comportement inapproprié");
|
||||
|
||||
assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.SUSPENDU);
|
||||
assertThat(result.getMotifStatut()).isEqualTo("comportement inapproprié");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("suspendreMembre: motif null → motif par défaut")
|
||||
void suspendreMembre_nullMotif_usesDefaultMotif() {
|
||||
var lien = buildLien(StatutMembre.ACTIF);
|
||||
when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
|
||||
doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));
|
||||
|
||||
var result = service.suspendreMembre(lien.getId(), UUID.randomUUID(), null);
|
||||
|
||||
assertThat(result.getMotifStatut()).isEqualTo("Suspension par l'administrateur");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("suspendreMembre: INVITE → IllegalStateException")
|
||||
void suspendreMembre_invite_throwsIllegalState() {
|
||||
var lien = buildLien(StatutMembre.INVITE);
|
||||
when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
|
||||
|
||||
assertThatThrownBy(() -> service.suspendreMembre(lien.getId(), UUID.randomUUID(), null))
|
||||
.isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("suspendreMembre: EN_ATTENTE_VALIDATION → IllegalStateException")
|
||||
void suspendreMembre_enAttente_throwsIllegalState() {
|
||||
var lien = buildLien(StatutMembre.EN_ATTENTE_VALIDATION);
|
||||
when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
|
||||
|
||||
assertThatThrownBy(() -> service.suspendreMembre(lien.getId(), UUID.randomUUID(), null))
|
||||
.isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// radierMembre
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@DisplayName("radierMembre: ACTIF → RADIE")
|
||||
void radierMembre_actif_becomesRadie() {
|
||||
var lien = buildLien(StatutMembre.ACTIF);
|
||||
when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
|
||||
doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));
|
||||
|
||||
var result = service.radierMembre(lien.getId(), UUID.randomUUID(), "grave faute");
|
||||
|
||||
assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.RADIE);
|
||||
assertThat(result.getMotifStatut()).isEqualTo("grave faute");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("radierMembre: SUSPENDU → RADIE (radiation depuis suspension)")
|
||||
void radierMembre_suspendu_becomesRadie() {
|
||||
var lien = buildLien(StatutMembre.SUSPENDU);
|
||||
when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
|
||||
doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));
|
||||
|
||||
var result = service.radierMembre(lien.getId(), UUID.randomUUID(), null);
|
||||
|
||||
assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.RADIE);
|
||||
assertThat(result.getMotifStatut()).isEqualTo("Radiation par l'administrateur");
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// archiverMembre
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@DisplayName("archiverMembre: RADIE → ARCHIVE")
|
||||
void archiverMembre_radie_becomesArchive() {
|
||||
var lien = buildLien(StatutMembre.RADIE);
|
||||
when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
|
||||
doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));
|
||||
|
||||
var result = service.archiverMembre(lien.getId(), "nettoyage annuel");
|
||||
|
||||
assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.ARCHIVE);
|
||||
assertThat(result.getMotifArchivage()).isEqualTo("nettoyage annuel");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("archiverMembre: ACTIF → ARCHIVE (archivage direct)")
|
||||
void archiverMembre_actif_becomesArchive() {
|
||||
var lien = buildLien(StatutMembre.ACTIF);
|
||||
when(membreOrgRepository.findByIdOptional(lien.getId())).thenReturn(Optional.of(lien));
|
||||
doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));
|
||||
|
||||
var result = service.archiverMembre(lien.getId(), null);
|
||||
|
||||
assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.ARCHIVE);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// inviterMembre
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@DisplayName("inviterMembre: crée un lien INVITE avec token 32 chars et expiration 7j")
|
||||
void inviterMembre_createsInviteLienWithToken() {
|
||||
Membre membre = new Membre();
|
||||
membre.setId(UUID.randomUUID());
|
||||
membre.setNom("Sow");
|
||||
membre.setPrenom("Amadou");
|
||||
|
||||
Organisation org = new Organisation();
|
||||
org.setId(UUID.randomUUID());
|
||||
org.setNom("Coopérative Agricole");
|
||||
|
||||
when(membreOrgRepository.findByMembreIdAndOrganisationId(membre.getId(), org.getId()))
|
||||
.thenReturn(Optional.empty());
|
||||
doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));
|
||||
|
||||
var result = service.inviterMembre(membre, org, UUID.randomUUID(), "TRESORIER");
|
||||
|
||||
assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.INVITE);
|
||||
assertThat(result.getTokenInvitation()).isNotNull().hasSize(32);
|
||||
assertThat(result.getRoleOrg()).isEqualTo("TRESORIER");
|
||||
assertThat(result.getDateExpirationInvitation())
|
||||
.isAfter(LocalDateTime.now().plusDays(6));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("inviterMembre: roleOrg null accepté")
|
||||
void inviterMembre_nullRoleOrg_accepted() {
|
||||
Membre membre = new Membre();
|
||||
membre.setId(UUID.randomUUID());
|
||||
membre.setNom("Diallo");
|
||||
membre.setPrenom("Fatou");
|
||||
Organisation org = new Organisation();
|
||||
org.setId(UUID.randomUUID());
|
||||
org.setNom("Tontine");
|
||||
|
||||
when(membreOrgRepository.findByMembreIdAndOrganisationId(any(), any()))
|
||||
.thenReturn(Optional.empty());
|
||||
doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));
|
||||
|
||||
var result = service.inviterMembre(membre, org, UUID.randomUUID(), null);
|
||||
|
||||
assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.INVITE);
|
||||
assertThat(result.getRoleOrg()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("inviterMembre: membre déjà lié → IllegalStateException")
|
||||
void inviterMembre_alreadyLinked_throwsIllegalState() {
|
||||
Membre membre = new Membre();
|
||||
membre.setId(UUID.randomUUID());
|
||||
membre.setNom("Test");
|
||||
membre.setPrenom("Test");
|
||||
Organisation org = new Organisation();
|
||||
org.setId(UUID.randomUUID());
|
||||
org.setNom("Org");
|
||||
|
||||
var existingLien = buildLien(StatutMembre.ACTIF);
|
||||
when(membreOrgRepository.findByMembreIdAndOrganisationId(membre.getId(), org.getId()))
|
||||
.thenReturn(Optional.of(existingLien));
|
||||
|
||||
assertThatThrownBy(() -> service.inviterMembre(membre, org, UUID.randomUUID(), null))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("déjà lié");
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// accepterInvitation
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@DisplayName("accepterInvitation: token valide → EN_ATTENTE_VALIDATION, token invalidé")
|
||||
@SuppressWarnings("unchecked")
|
||||
void accepterInvitation_validToken_becomesEnAttenteValidation() {
|
||||
String token = "abc123validtokenabcde1234567890a";
|
||||
var lien = buildLien(StatutMembre.INVITE);
|
||||
lien.setTokenInvitation(token);
|
||||
lien.setDateExpirationInvitation(LocalDateTime.now().plusDays(7));
|
||||
|
||||
PanacheQuery<MembreOrganisation> mockQuery = mock(PanacheQuery.class);
|
||||
when(membreOrgRepository.find(anyString(), eq(token), eq(StatutMembre.INVITE)))
|
||||
.thenReturn(mockQuery);
|
||||
when(mockQuery.firstResultOptional()).thenReturn(Optional.of(lien));
|
||||
doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));
|
||||
|
||||
var result = service.accepterInvitation(token);
|
||||
|
||||
assertThat(result.getStatutMembre()).isEqualTo(StatutMembre.EN_ATTENTE_VALIDATION);
|
||||
assertThat(result.getTokenInvitation()).isNull(); // Token invalidé
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("accepterInvitation: invitation expirée → IllegalStateException")
|
||||
@SuppressWarnings("unchecked")
|
||||
void accepterInvitation_expiredToken_throwsIllegalState() {
|
||||
String token = "expiredtokenabcde1234567890abc12";
|
||||
var lien = buildLien(StatutMembre.INVITE);
|
||||
lien.setTokenInvitation(token);
|
||||
lien.setDateExpirationInvitation(LocalDateTime.now().minusDays(1)); // Expiré hier
|
||||
|
||||
PanacheQuery<MembreOrganisation> mockQuery = mock(PanacheQuery.class);
|
||||
when(membreOrgRepository.find(anyString(), eq(token), eq(StatutMembre.INVITE)))
|
||||
.thenReturn(mockQuery);
|
||||
when(mockQuery.firstResultOptional()).thenReturn(Optional.of(lien));
|
||||
|
||||
assertThatThrownBy(() -> service.accepterInvitation(token))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("expir");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("accepterInvitation: token inexistant → IllegalArgumentException")
|
||||
@SuppressWarnings("unchecked")
|
||||
void accepterInvitation_unknownToken_throwsIllegalArgument() {
|
||||
String token = "unknowntoken0000000000000000000";
|
||||
|
||||
PanacheQuery<MembreOrganisation> mockQuery = mock(PanacheQuery.class);
|
||||
when(membreOrgRepository.find(anyString(), eq(token), eq(StatutMembre.INVITE)))
|
||||
.thenReturn(mockQuery);
|
||||
when(mockQuery.firstResultOptional()).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> service.accepterInvitation(token))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Invitation introuvable");
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// expirerInvitations (automatique)
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@DisplayName("expirerInvitations: expire 3 invitations périmées → retourne 3")
|
||||
void expirerInvitations_expiresAll_returnsCount() {
|
||||
var lien1 = buildLien(StatutMembre.INVITE);
|
||||
var lien2 = buildLien(StatutMembre.INVITE);
|
||||
var lien3 = buildLien(StatutMembre.INVITE);
|
||||
|
||||
when(membreOrgRepository.findInvitationsExpirees(any(LocalDateTime.class)))
|
||||
.thenReturn(List.of(lien1, lien2, lien3));
|
||||
doNothing().when(membreOrgRepository).persist(any(MembreOrganisation.class));
|
||||
|
||||
int count = service.expirerInvitations();
|
||||
|
||||
assertThat(count).isEqualTo(3);
|
||||
assertThat(lien1.getStatutMembre()).isEqualTo(StatutMembre.RADIE);
|
||||
assertThat(lien2.getStatutMembre()).isEqualTo(StatutMembre.RADIE);
|
||||
assertThat(lien3.getStatutMembre()).isEqualTo(StatutMembre.RADIE);
|
||||
assertThat(lien1.getMotifStatut()).isEqualTo("Invitation expirée sans réponse");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("expirerInvitations: aucune invitation expirée → retourne 0")
|
||||
void expirerInvitations_noneExpired_returnsZero() {
|
||||
when(membreOrgRepository.findInvitationsExpirees(any(LocalDateTime.class)))
|
||||
.thenReturn(List.of());
|
||||
|
||||
int count = service.expirerInvitations();
|
||||
|
||||
assertThat(count).isZero();
|
||||
verify(membreOrgRepository, never()).persist(any(MembreOrganisation.class));
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
// envoyerRappelsInvitation (automatique)
|
||||
// ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Test
|
||||
@DisplayName("envoyerRappelsInvitation: 2 invitations expirant bientôt → retourne 2")
|
||||
void envoyerRappelsInvitation_twoExpiringSoon_sendsTwo() {
|
||||
var lien1 = buildLien(StatutMembre.INVITE);
|
||||
var lien2 = buildLien(StatutMembre.INVITE);
|
||||
|
||||
when(membreOrgRepository.findInvitationsExpirantBientot(
|
||||
any(LocalDateTime.class), any(LocalDateTime.class)))
|
||||
.thenReturn(List.of(lien1, lien2));
|
||||
|
||||
int count = service.envoyerRappelsInvitation();
|
||||
|
||||
assertThat(count).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("envoyerRappelsInvitation: aucune invitation → retourne 0")
|
||||
void envoyerRappelsInvitation_none_returnsZero() {
|
||||
when(membreOrgRepository.findInvitationsExpirantBientot(
|
||||
any(LocalDateTime.class), any(LocalDateTime.class)))
|
||||
.thenReturn(List.of());
|
||||
|
||||
int count = service.envoyerRappelsInvitation();
|
||||
|
||||
assertThat(count).isZero();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user