feat(mobile): Implement Keycloak WebView authentication with HTTP callback

- Replace flutter_appauth with custom WebView implementation to resolve deep link issues
- Add KeycloakWebViewAuthService with integrated WebView for seamless authentication
- Configure Android manifest for HTTP cleartext traffic support
- Add network security config for development environment (192.168.1.11)
- Update Keycloak client to use HTTP callback endpoint (http://192.168.1.11:8080/auth/callback)
- Remove obsolete keycloak_auth_service.dart and temporary scripts
- Clean up dependencies and regenerate injection configuration
- Tested successfully on multiple Android devices (Xiaomi 2201116TG, SM A725F)

BREAKING CHANGE: Authentication flow now uses WebView instead of external browser
- Users will see Keycloak login page within the app instead of browser redirect
- Resolves ERR_CLEARTEXT_NOT_PERMITTED and deep link state management issues
- Maintains full OIDC compliance with PKCE flow and secure token storage

Technical improvements:
- WebView with custom navigation delegate for callback handling
- Automatic token extraction and user info parsing from JWT
- Proper error handling and user feedback
- Consistent authentication state management across app lifecycle
This commit is contained in:
DahoudG
2025-09-15 01:44:16 +00:00
parent 73459b3092
commit f89f6167cc
290 changed files with 34563 additions and 3528 deletions

View File

@@ -0,0 +1,413 @@
package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.entity.Evenement;
import dev.lions.unionflow.server.entity.Evenement.StatutEvenement;
import dev.lions.unionflow.server.entity.Evenement.TypeEvenement;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.restassured.http.ContentType;
import jakarta.transaction.Transactional;
import org.junit.jupiter.api.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
/**
* Tests d'intégration pour EvenementResource
*
* Tests complets de l'API REST des événements avec authentification
* et validation des permissions. Optimisé pour l'intégration mobile.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-15
*/
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@DisplayName("Tests d'intégration - API Événements")
class EvenementResourceTest {
private static Long evenementTestId;
private static Long organisationTestId;
private static Long membreTestId;
@BeforeAll
@Transactional
static void setupTestData() {
// Créer une organisation de test
Organisation organisation = Organisation.builder()
.nom("Union Test API")
.typeOrganisation("ASSOCIATION")
.statut("ACTIVE")
.email("test-api@union.com")
.telephone("0123456789")
.adresse("123 Rue de Test")
.codePostal("75001")
.ville("Paris")
.pays("France")
.actif(true)
.creePar("test@unionflow.dev")
.dateCreation(LocalDateTime.now())
.build();
organisation.persist();
organisationTestId = organisation.id;
// Créer un membre de test
Membre membre = Membre.builder()
.numeroMembre("UF2025-API01")
.prenom("Marie")
.nom("Martin")
.email("marie.martin@test.com")
.telephone("0987654321")
.dateNaissance(LocalDate.of(1990, 5, 15))
.dateAdhesion(LocalDate.now())
.actif(true)
.organisation(organisation)
.build();
membre.persist();
membreTestId = membre.id;
// Créer un événement de test
Evenement evenement = Evenement.builder()
.titre("Conférence API Test")
.description("Conférence de test pour l'API")
.dateDebut(LocalDateTime.now().plusDays(15))
.dateFin(LocalDateTime.now().plusDays(15).plusHours(2))
.lieu("Centre de conférence Test")
.typeEvenement(TypeEvenement.CONFERENCE)
.statut(StatutEvenement.PLANIFIE)
.capaciteMax(50)
.prix(BigDecimal.valueOf(15.00))
.inscriptionRequise(true)
.visiblePublic(true)
.actif(true)
.organisation(organisation)
.organisateur(membre)
.creePar("test@unionflow.dev")
.dateCreation(LocalDateTime.now())
.build();
evenement.persist();
evenementTestId = evenement.id;
}
@Test
@Order(1)
@DisplayName("GET /api/evenements - Lister événements (authentifié)")
@TestSecurity(user = "marie.martin@test.com", roles = {"MEMBRE"})
void testListerEvenements_Authentifie() {
given()
.when()
.get("/api/evenements")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("size()", greaterThanOrEqualTo(1))
.body("[0].titre", notNullValue())
.body("[0].dateDebut", notNullValue())
.body("[0].statut", notNullValue());
}
@Test
@Order(2)
@DisplayName("GET /api/evenements - Non authentifié")
void testListerEvenements_NonAuthentifie() {
given()
.when()
.get("/api/evenements")
.then()
.statusCode(401);
}
@Test
@Order(3)
@DisplayName("GET /api/evenements/{id} - Récupérer événement")
@TestSecurity(user = "marie.martin@test.com", roles = {"MEMBRE"})
void testObtenirEvenement() {
given()
.pathParam("id", evenementTestId)
.when()
.get("/api/evenements/{id}")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("id", equalTo(evenementTestId.intValue()))
.body("titre", equalTo("Conférence API Test"))
.body("description", equalTo("Conférence de test pour l'API"))
.body("typeEvenement", equalTo("CONFERENCE"))
.body("statut", equalTo("PLANIFIE"))
.body("capaciteMax", equalTo(50))
.body("prix", equalTo(15.0f))
.body("inscriptionRequise", equalTo(true))
.body("visiblePublic", equalTo(true))
.body("actif", equalTo(true));
}
@Test
@Order(4)
@DisplayName("GET /api/evenements/{id} - Événement non trouvé")
@TestSecurity(user = "marie.martin@test.com", roles = {"MEMBRE"})
void testObtenirEvenement_NonTrouve() {
given()
.pathParam("id", 99999)
.when()
.get("/api/evenements/{id}")
.then()
.statusCode(404)
.body("error", equalTo("Événement non trouvé"));
}
@Test
@Order(5)
@DisplayName("POST /api/evenements - Créer événement (organisateur)")
@TestSecurity(user = "marie.martin@test.com", roles = {"ORGANISATEUR_EVENEMENT"})
void testCreerEvenement_Organisateur() {
String nouvelEvenement = String.format("""
{
"titre": "Nouvel Événement Test",
"description": "Description du nouvel événement",
"dateDebut": "%s",
"dateFin": "%s",
"lieu": "Lieu de test",
"typeEvenement": "FORMATION",
"capaciteMax": 30,
"prix": 20.00,
"inscriptionRequise": true,
"visiblePublic": true,
"organisation": {"id": %d},
"organisateur": {"id": %d}
}
""",
LocalDateTime.now().plusDays(20).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
LocalDateTime.now().plusDays(20).plusHours(3).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
organisationTestId,
membreTestId
);
given()
.contentType(ContentType.JSON)
.body(nouvelEvenement)
.when()
.post("/api/evenements")
.then()
.statusCode(201)
.contentType(ContentType.JSON)
.body("titre", equalTo("Nouvel Événement Test"))
.body("typeEvenement", equalTo("FORMATION"))
.body("capaciteMax", equalTo(30))
.body("prix", equalTo(20.0f))
.body("actif", equalTo(true));
}
@Test
@Order(6)
@DisplayName("POST /api/evenements - Permissions insuffisantes")
@TestSecurity(user = "marie.martin@test.com", roles = {"MEMBRE"})
void testCreerEvenement_PermissionsInsuffisantes() {
String nouvelEvenement = """
{
"titre": "Événement Non Autorisé",
"description": "Test permissions",
"dateDebut": "2025-02-15T10:00:00",
"dateFin": "2025-02-15T12:00:00",
"lieu": "Lieu test",
"typeEvenement": "FORMATION"
}
""";
given()
.contentType(ContentType.JSON)
.body(nouvelEvenement)
.when()
.post("/api/evenements")
.then()
.statusCode(403);
}
@Test
@Order(7)
@DisplayName("PUT /api/evenements/{id} - Mettre à jour événement")
@TestSecurity(user = "admin@unionflow.dev", roles = {"ADMIN"})
void testMettreAJourEvenement_Admin() {
String evenementModifie = String.format("""
{
"titre": "Conférence API Test - Modifiée",
"description": "Description mise à jour",
"dateDebut": "%s",
"dateFin": "%s",
"lieu": "Nouveau lieu",
"typeEvenement": "CONFERENCE",
"capaciteMax": 75,
"prix": 25.00,
"inscriptionRequise": true,
"visiblePublic": true
}
""",
LocalDateTime.now().plusDays(16).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
LocalDateTime.now().plusDays(16).plusHours(3).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
);
given()
.pathParam("id", evenementTestId)
.contentType(ContentType.JSON)
.body(evenementModifie)
.when()
.put("/api/evenements/{id}")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("titre", equalTo("Conférence API Test - Modifiée"))
.body("description", equalTo("Description mise à jour"))
.body("lieu", equalTo("Nouveau lieu"))
.body("capaciteMax", equalTo(75))
.body("prix", equalTo(25.0f));
}
@Test
@Order(8)
@DisplayName("GET /api/evenements/a-venir - Événements à venir")
@TestSecurity(user = "marie.martin@test.com", roles = {"MEMBRE"})
void testEvenementsAVenir() {
given()
.queryParam("page", 0)
.queryParam("size", 10)
.when()
.get("/api/evenements/a-venir")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("size()", greaterThanOrEqualTo(0));
}
@Test
@Order(9)
@DisplayName("GET /api/evenements/publics - Événements publics (non authentifié)")
void testEvenementsPublics_NonAuthentifie() {
given()
.queryParam("page", 0)
.queryParam("size", 20)
.when()
.get("/api/evenements/publics")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("size()", greaterThanOrEqualTo(0));
}
@Test
@Order(10)
@DisplayName("GET /api/evenements/recherche - Recherche d'événements")
@TestSecurity(user = "marie.martin@test.com", roles = {"MEMBRE"})
void testRechercherEvenements() {
given()
.queryParam("q", "Conférence")
.queryParam("page", 0)
.queryParam("size", 20)
.when()
.get("/api/evenements/recherche")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("size()", greaterThanOrEqualTo(0));
}
@Test
@Order(11)
@DisplayName("GET /api/evenements/recherche - Terme de recherche manquant")
@TestSecurity(user = "marie.martin@test.com", roles = {"MEMBRE"})
void testRechercherEvenements_TermeManquant() {
given()
.queryParam("page", 0)
.queryParam("size", 20)
.when()
.get("/api/evenements/recherche")
.then()
.statusCode(400)
.body("error", equalTo("Le terme de recherche est obligatoire"));
}
@Test
@Order(12)
@DisplayName("GET /api/evenements/type/{type} - Événements par type")
@TestSecurity(user = "marie.martin@test.com", roles = {"MEMBRE"})
void testEvenementsParType() {
given()
.pathParam("type", "CONFERENCE")
.queryParam("page", 0)
.queryParam("size", 20)
.when()
.get("/api/evenements/type/{type}")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("size()", greaterThanOrEqualTo(0));
}
@Test
@Order(13)
@DisplayName("PATCH /api/evenements/{id}/statut - Changer statut")
@TestSecurity(user = "admin@unionflow.dev", roles = {"ADMIN"})
void testChangerStatut() {
given()
.pathParam("id", evenementTestId)
.queryParam("statut", "CONFIRME")
.when()
.patch("/api/evenements/{id}/statut")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("statut", equalTo("CONFIRME"));
}
@Test
@Order(14)
@DisplayName("GET /api/evenements/statistiques - Statistiques")
@TestSecurity(user = "admin@unionflow.dev", roles = {"ADMIN"})
void testObtenirStatistiques() {
given()
.when()
.get("/api/evenements/statistiques")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("total", notNullValue())
.body("actifs", notNullValue())
.body("timestamp", notNullValue());
}
@Test
@Order(15)
@DisplayName("DELETE /api/evenements/{id} - Supprimer événement")
@TestSecurity(user = "admin@unionflow.dev", roles = {"ADMIN"})
void testSupprimerEvenement() {
given()
.pathParam("id", evenementTestId)
.when()
.delete("/api/evenements/{id}")
.then()
.statusCode(204);
}
@Test
@Order(16)
@DisplayName("Pagination - Paramètres valides")
@TestSecurity(user = "marie.martin@test.com", roles = {"MEMBRE"})
void testPagination() {
given()
.queryParam("page", 0)
.queryParam("size", 5)
.queryParam("sort", "titre")
.queryParam("direction", "asc")
.when()
.get("/api/evenements")
.then()
.statusCode(200)
.contentType(ContentType.JSON);
}
}

View File

@@ -0,0 +1,335 @@
package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.api.dto.organisation.OrganisationDTO;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.UUID;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
/**
* Tests d'intégration pour OrganisationResource
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-15
*/
@QuarkusTest
class OrganisationResourceTest {
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testCreerOrganisation_Success() {
OrganisationDTO organisation = createTestOrganisationDTO();
given()
.contentType(ContentType.JSON)
.body(organisation)
.when()
.post("/api/organisations")
.then()
.statusCode(201)
.body("nom", equalTo("Lions Club Test API"))
.body("email", equalTo("testapi@lionsclub.org"))
.body("actif", equalTo(true));
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testCreerOrganisation_EmailInvalide() {
OrganisationDTO organisation = createTestOrganisationDTO();
organisation.setEmail("email-invalide");
given()
.contentType(ContentType.JSON)
.body(organisation)
.when()
.post("/api/organisations")
.then()
.statusCode(400);
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testCreerOrganisation_NomVide() {
OrganisationDTO organisation = createTestOrganisationDTO();
organisation.setNom("");
given()
.contentType(ContentType.JSON)
.body(organisation)
.when()
.post("/api/organisations")
.then()
.statusCode(400);
}
@Test
void testCreerOrganisation_NonAuthentifie() {
OrganisationDTO organisation = createTestOrganisationDTO();
given()
.contentType(ContentType.JSON)
.body(organisation)
.when()
.post("/api/organisations")
.then()
.statusCode(401);
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testListerOrganisations_Success() {
given()
.when()
.get("/api/organisations")
.then()
.statusCode(200)
.body("size()", greaterThanOrEqualTo(0));
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testListerOrganisations_AvecPagination() {
given()
.queryParam("page", 0)
.queryParam("size", 10)
.when()
.get("/api/organisations")
.then()
.statusCode(200)
.body("size()", greaterThanOrEqualTo(0));
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testListerOrganisations_AvecRecherche() {
given()
.queryParam("recherche", "Lions")
.when()
.get("/api/organisations")
.then()
.statusCode(200)
.body("size()", greaterThanOrEqualTo(0));
}
@Test
void testListerOrganisations_NonAuthentifie() {
given()
.when()
.get("/api/organisations")
.then()
.statusCode(401);
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testObtenirOrganisation_NonTrouvee() {
given()
.when()
.get("/api/organisations/99999")
.then()
.statusCode(404)
.body("error", equalTo("Organisation non trouvée"));
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testMettreAJourOrganisation_NonTrouvee() {
OrganisationDTO organisation = createTestOrganisationDTO();
given()
.contentType(ContentType.JSON)
.body(organisation)
.when()
.put("/api/organisations/99999")
.then()
.statusCode(404)
.body("error", containsString("Organisation non trouvée"));
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testSupprimerOrganisation_NonTrouvee() {
given()
.when()
.delete("/api/organisations/99999")
.then()
.statusCode(404)
.body("error", containsString("Organisation non trouvée"));
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testRechercheAvancee_Success() {
given()
.queryParam("nom", "Lions")
.queryParam("ville", "Abidjan")
.queryParam("page", 0)
.queryParam("size", 10)
.when()
.get("/api/organisations/recherche")
.then()
.statusCode(200)
.body("size()", greaterThanOrEqualTo(0));
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testRechercheAvancee_SansCriteres() {
given()
.queryParam("page", 0)
.queryParam("size", 10)
.when()
.get("/api/organisations/recherche")
.then()
.statusCode(200)
.body("size()", greaterThanOrEqualTo(0));
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testActiverOrganisation_NonTrouvee() {
given()
.when()
.post("/api/organisations/99999/activer")
.then()
.statusCode(404)
.body("error", containsString("Organisation non trouvée"));
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testSuspendreOrganisation_NonTrouvee() {
given()
.when()
.post("/api/organisations/99999/suspendre")
.then()
.statusCode(404)
.body("error", containsString("Organisation non trouvée"));
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testObtenirStatistiques_Success() {
given()
.when()
.get("/api/organisations/statistiques")
.then()
.statusCode(200)
.body("totalOrganisations", notNullValue())
.body("organisationsActives", notNullValue())
.body("organisationsInactives", notNullValue())
.body("nouvellesOrganisations30Jours", notNullValue())
.body("tauxActivite", notNullValue())
.body("timestamp", notNullValue());
}
@Test
void testObtenirStatistiques_NonAuthentifie() {
given()
.when()
.get("/api/organisations/statistiques")
.then()
.statusCode(401);
}
/**
* Test de workflow complet : création, lecture, mise à jour, suppression
*/
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testWorkflowComplet() {
// 1. Créer une organisation
OrganisationDTO organisation = createTestOrganisationDTO();
organisation.setNom("Lions Club Workflow Test");
organisation.setEmail("workflow@lionsclub.org");
String location = given()
.contentType(ContentType.JSON)
.body(organisation)
.when()
.post("/api/organisations")
.then()
.statusCode(201)
.extract()
.header("Location");
// Extraire l'ID de l'organisation créée
String organisationId = location.substring(location.lastIndexOf("/") + 1);
// 2. Lire l'organisation créée
given()
.when()
.get("/api/organisations/" + organisationId)
.then()
.statusCode(200)
.body("nom", equalTo("Lions Club Workflow Test"))
.body("email", equalTo("workflow@lionsclub.org"));
// 3. Mettre à jour l'organisation
organisation.setDescription("Description mise à jour");
given()
.contentType(ContentType.JSON)
.body(organisation)
.when()
.put("/api/organisations/" + organisationId)
.then()
.statusCode(200)
.body("description", equalTo("Description mise à jour"));
// 4. Suspendre l'organisation
given()
.when()
.post("/api/organisations/" + organisationId + "/suspendre")
.then()
.statusCode(200);
// 5. Activer l'organisation
given()
.when()
.post("/api/organisations/" + organisationId + "/activer")
.then()
.statusCode(200);
// 6. Supprimer l'organisation (soft delete)
given()
.when()
.delete("/api/organisations/" + organisationId)
.then()
.statusCode(204);
}
/**
* Crée un DTO d'organisation pour les tests
*/
private OrganisationDTO createTestOrganisationDTO() {
OrganisationDTO dto = new OrganisationDTO();
dto.setId(UUID.randomUUID());
dto.setNom("Lions Club Test API");
dto.setNomCourt("LC Test API");
dto.setEmail("testapi@lionsclub.org");
dto.setDescription("Organisation de test pour l'API");
dto.setTelephone("+225 01 02 03 04 05");
dto.setAdresse("123 Rue de Test API");
dto.setVille("Abidjan");
dto.setCodePostal("00225");
dto.setRegion("Lagunes");
dto.setPays("Côte d'Ivoire");
dto.setSiteWeb("https://testapi.lionsclub.org");
dto.setObjectifs("Servir la communauté");
dto.setActivitesPrincipales("Actions sociales et humanitaires");
dto.setNombreMembres(0);
dto.setDateCreation(LocalDateTime.now());
dto.setActif(true);
dto.setVersion(0L);
return dto;
}
}

View File

@@ -0,0 +1,408 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.entity.Evenement;
import dev.lions.unionflow.server.entity.Evenement.StatutEvenement;
import dev.lions.unionflow.server.entity.Evenement.TypeEvenement;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.EvenementRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectMock;
import jakarta.inject.Inject;
import org.junit.jupiter.api.*;
import org.mockito.Mockito;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* Tests unitaires pour EvenementService
*
* Tests complets du service de gestion des événements avec
* validation des règles métier et intégration Keycloak.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-15
*/
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@DisplayName("Tests unitaires - Service Événements")
class EvenementServiceTest {
@Inject
EvenementService evenementService;
@InjectMock
EvenementRepository evenementRepository;
@InjectMock
MembreRepository membreRepository;
@InjectMock
OrganisationRepository organisationRepository;
@InjectMock
KeycloakService keycloakService;
private Evenement evenementTest;
private Organisation organisationTest;
private Membre membreTest;
@BeforeEach
void setUp() {
// Données de test
organisationTest = Organisation.builder()
.nom("Union Test")
.typeOrganisation("ASSOCIATION")
.statut("ACTIVE")
.email("test@union.com")
.actif(true)
.build();
organisationTest.id = 1L;
membreTest = Membre.builder()
.numeroMembre("UF2025-TEST01")
.prenom("Jean")
.nom("Dupont")
.email("jean.dupont@test.com")
.actif(true)
.build();
membreTest.id = 1L;
evenementTest = Evenement.builder()
.titre("Assemblée Générale 2025")
.description("Assemblée générale annuelle de l'union")
.dateDebut(LocalDateTime.now().plusDays(30))
.dateFin(LocalDateTime.now().plusDays(30).plusHours(3))
.lieu("Salle de conférence")
.typeEvenement(TypeEvenement.ASSEMBLEE_GENERALE)
.statut(StatutEvenement.PLANIFIE)
.capaciteMax(100)
.prix(BigDecimal.valueOf(25.00))
.inscriptionRequise(true)
.visiblePublic(true)
.actif(true)
.organisation(organisationTest)
.organisateur(membreTest)
.build();
evenementTest.id = 1L;
}
@Test
@Order(1)
@DisplayName("Création d'événement - Succès")
void testCreerEvenement_Succes() {
// Given
when(keycloakService.getCurrentUserEmail()).thenReturn("jean.dupont@test.com");
when(evenementRepository.findByTitre(anyString())).thenReturn(Optional.empty());
doNothing().when(evenementRepository).persist(any(Evenement.class));
// When
Evenement resultat = evenementService.creerEvenement(evenementTest);
// Then
assertNotNull(resultat);
assertEquals("Assemblée Générale 2025", resultat.getTitre());
assertEquals(StatutEvenement.PLANIFIE, resultat.getStatut());
assertTrue(resultat.getActif());
assertEquals("jean.dupont@test.com", resultat.getCreePar());
verify(evenementRepository).persist(any(Evenement.class));
}
@Test
@Order(2)
@DisplayName("Création d'événement - Titre obligatoire")
void testCreerEvenement_TitreObligatoire() {
// Given
evenementTest.setTitre(null);
// When & Then
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> evenementService.creerEvenement(evenementTest)
);
assertEquals("Le titre de l'événement est obligatoire", exception.getMessage());
verify(evenementRepository, never()).persist(any(Evenement.class));
}
@Test
@Order(3)
@DisplayName("Création d'événement - Date de début obligatoire")
void testCreerEvenement_DateDebutObligatoire() {
// Given
evenementTest.setDateDebut(null);
// When & Then
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> evenementService.creerEvenement(evenementTest)
);
assertEquals("La date de début est obligatoire", exception.getMessage());
}
@Test
@Order(4)
@DisplayName("Création d'événement - Date de début dans le passé")
void testCreerEvenement_DateDebutPassee() {
// Given
evenementTest.setDateDebut(LocalDateTime.now().minusDays(1));
// When & Then
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> evenementService.creerEvenement(evenementTest)
);
assertEquals("La date de début ne peut pas être dans le passé", exception.getMessage());
}
@Test
@Order(5)
@DisplayName("Création d'événement - Date de fin antérieure à date de début")
void testCreerEvenement_DateFinInvalide() {
// Given
evenementTest.setDateFin(evenementTest.getDateDebut().minusHours(1));
// When & Then
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> evenementService.creerEvenement(evenementTest)
);
assertEquals("La date de fin ne peut pas être antérieure à la date de début", exception.getMessage());
}
@Test
@Order(6)
@DisplayName("Mise à jour d'événement - Succès")
void testMettreAJourEvenement_Succes() {
// Given
when(evenementRepository.findByIdOptional(1L)).thenReturn(Optional.of(evenementTest));
when(keycloakService.hasRole("ADMIN")).thenReturn(true);
when(keycloakService.getCurrentUserEmail()).thenReturn("admin@test.com");
doNothing().when(evenementRepository).persist(any(Evenement.class));
Evenement evenementMisAJour = Evenement.builder()
.titre("Assemblée Générale 2025 - Modifiée")
.description("Description mise à jour")
.dateDebut(LocalDateTime.now().plusDays(35))
.dateFin(LocalDateTime.now().plusDays(35).plusHours(4))
.lieu("Nouvelle salle")
.typeEvenement(TypeEvenement.ASSEMBLEE_GENERALE)
.capaciteMax(150)
.prix(BigDecimal.valueOf(30.00))
.inscriptionRequise(true)
.visiblePublic(true)
.build();
// When
Evenement resultat = evenementService.mettreAJourEvenement(1L, evenementMisAJour);
// Then
assertNotNull(resultat);
assertEquals("Assemblée Générale 2025 - Modifiée", resultat.getTitre());
assertEquals("Description mise à jour", resultat.getDescription());
assertEquals("Nouvelle salle", resultat.getLieu());
assertEquals(150, resultat.getCapaciteMax());
assertEquals(BigDecimal.valueOf(30.00), resultat.getPrix());
assertEquals("admin@test.com", resultat.getModifiePar());
verify(evenementRepository).persist(any(Evenement.class));
}
@Test
@Order(7)
@DisplayName("Mise à jour d'événement - Événement non trouvé")
void testMettreAJourEvenement_NonTrouve() {
// Given
when(evenementRepository.findByIdOptional(999L)).thenReturn(Optional.empty());
// When & Then
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> evenementService.mettreAJourEvenement(999L, evenementTest)
);
assertEquals("Événement non trouvé avec l'ID: 999", exception.getMessage());
}
@Test
@Order(8)
@DisplayName("Suppression d'événement - Succès")
void testSupprimerEvenement_Succes() {
// Given
when(evenementRepository.findByIdOptional(1L)).thenReturn(Optional.of(evenementTest));
when(keycloakService.hasRole("ADMIN")).thenReturn(true);
when(keycloakService.getCurrentUserEmail()).thenReturn("admin@test.com");
when(evenementTest.getNombreInscrits()).thenReturn(0);
doNothing().when(evenementRepository).persist(any(Evenement.class));
// When
assertDoesNotThrow(() -> evenementService.supprimerEvenement(1L));
// Then
assertFalse(evenementTest.getActif());
assertEquals("admin@test.com", evenementTest.getModifiePar());
verify(evenementRepository).persist(any(Evenement.class));
}
@Test
@Order(9)
@DisplayName("Recherche d'événements - Succès")
void testRechercherEvenements_Succes() {
// Given
List<Evenement> evenementsAttendus = List.of(evenementTest);
when(evenementRepository.findByTitreOrDescription(anyString(), any(Page.class), any(Sort.class)))
.thenReturn(evenementsAttendus);
// When
List<Evenement> resultat = evenementService.rechercherEvenements(
"Assemblée", Page.of(0, 10), Sort.by("dateDebut"));
// Then
assertNotNull(resultat);
assertEquals(1, resultat.size());
assertEquals("Assemblée Générale 2025", resultat.get(0).getTitre());
verify(evenementRepository).findByTitreOrDescription(eq("Assemblée"), any(Page.class), any(Sort.class));
}
@Test
@Order(10)
@DisplayName("Changement de statut - Succès")
void testChangerStatut_Succes() {
// Given
when(evenementRepository.findByIdOptional(1L)).thenReturn(Optional.of(evenementTest));
when(keycloakService.hasRole("ADMIN")).thenReturn(true);
when(keycloakService.getCurrentUserEmail()).thenReturn("admin@test.com");
doNothing().when(evenementRepository).persist(any(Evenement.class));
// When
Evenement resultat = evenementService.changerStatut(1L, StatutEvenement.CONFIRME);
// Then
assertNotNull(resultat);
assertEquals(StatutEvenement.CONFIRME, resultat.getStatut());
assertEquals("admin@test.com", resultat.getModifiePar());
verify(evenementRepository).persist(any(Evenement.class));
}
@Test
@Order(11)
@DisplayName("Statistiques des événements")
void testObtenirStatistiques() {
// Given
Map<String, Long> statsBase = Map.of(
"total", 100L,
"actifs", 80L,
"aVenir", 30L,
"enCours", 5L,
"passes", 45L,
"publics", 70L,
"avecInscription", 25L
);
when(evenementRepository.getStatistiques()).thenReturn(statsBase);
// When
Map<String, Object> resultat = evenementService.obtenirStatistiques();
// Then
assertNotNull(resultat);
assertEquals(100L, resultat.get("total"));
assertEquals(80L, resultat.get("actifs"));
assertEquals(30L, resultat.get("aVenir"));
assertEquals(80.0, resultat.get("tauxActivite"));
assertEquals(37.5, resultat.get("tauxEvenementsAVenir"));
assertEquals(6.25, resultat.get("tauxEvenementsEnCours"));
assertNotNull(resultat.get("timestamp"));
verify(evenementRepository).getStatistiques();
}
@Test
@Order(12)
@DisplayName("Lister événements actifs avec pagination")
void testListerEvenementsActifs() {
// Given
List<Evenement> evenementsAttendus = List.of(evenementTest);
when(evenementRepository.findAllActifs(any(Page.class), any(Sort.class)))
.thenReturn(evenementsAttendus);
// When
List<Evenement> resultat = evenementService.listerEvenementsActifs(
Page.of(0, 20), Sort.by("dateDebut"));
// Then
assertNotNull(resultat);
assertEquals(1, resultat.size());
assertEquals("Assemblée Générale 2025", resultat.get(0).getTitre());
verify(evenementRepository).findAllActifs(any(Page.class), any(Sort.class));
}
@Test
@Order(13)
@DisplayName("Validation des règles métier - Prix négatif")
void testValidation_PrixNegatif() {
// Given
evenementTest.setPrix(BigDecimal.valueOf(-10.00));
// When & Then
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> evenementService.creerEvenement(evenementTest)
);
assertEquals("Le prix ne peut pas être négatif", exception.getMessage());
}
@Test
@Order(14)
@DisplayName("Validation des règles métier - Capacité négative")
void testValidation_CapaciteNegative() {
// Given
evenementTest.setCapaciteMax(-5);
// When & Then
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> evenementService.creerEvenement(evenementTest)
);
assertEquals("La capacité maximale ne peut pas être négative", exception.getMessage());
}
@Test
@Order(15)
@DisplayName("Permissions - Utilisateur non autorisé")
void testPermissions_UtilisateurNonAutorise() {
// Given
when(evenementRepository.findByIdOptional(1L)).thenReturn(Optional.of(evenementTest));
when(keycloakService.hasRole(anyString())).thenReturn(false);
when(keycloakService.getCurrentUserEmail()).thenReturn("autre@test.com");
// When & Then
SecurityException exception = assertThrows(
SecurityException.class,
() -> evenementService.mettreAJourEvenement(1L, evenementTest)
);
assertEquals("Vous n'avez pas les permissions pour modifier cet événement", exception.getMessage());
}
}

View File

@@ -0,0 +1,346 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectMock;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* Tests unitaires pour OrganisationService
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-15
*/
@QuarkusTest
class OrganisationServiceTest {
@Inject
OrganisationService organisationService;
@InjectMock
OrganisationRepository organisationRepository;
private Organisation organisationTest;
@BeforeEach
void setUp() {
organisationTest = Organisation.builder()
.nom("Lions Club Test")
.nomCourt("LC Test")
.email("test@lionsclub.org")
.typeOrganisation("LIONS_CLUB")
.statut("ACTIVE")
.description("Organisation de test")
.telephone("+225 01 02 03 04 05")
.adresse("123 Rue de Test")
.ville("Abidjan")
.region("Lagunes")
.pays("Côte d'Ivoire")
.nombreMembres(25)
.actif(true)
.dateCreation(LocalDateTime.now())
.version(0L)
.build();
organisationTest.id = 1L;
}
@Test
void testCreerOrganisation_Success() {
// Given
Organisation organisationToCreate = Organisation.builder()
.nom("Lions Club Test New")
.email("testnew@lionsclub.org")
.typeOrganisation("LIONS_CLUB")
.build();
when(organisationRepository.findByEmail("testnew@lionsclub.org")).thenReturn(Optional.empty());
when(organisationRepository.findByNom("Lions Club Test New")).thenReturn(Optional.empty());
// When
Organisation result = organisationService.creerOrganisation(organisationToCreate);
// Then
assertNotNull(result);
assertEquals("Lions Club Test New", result.getNom());
assertEquals("ACTIVE", result.getStatut());
verify(organisationRepository).findByEmail("testnew@lionsclub.org");
verify(organisationRepository).findByNom("Lions Club Test New");
}
@Test
void testCreerOrganisation_EmailDejaExistant() {
// Given
when(organisationRepository.findByEmail(anyString())).thenReturn(Optional.of(organisationTest));
// When & Then
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> organisationService.creerOrganisation(organisationTest));
assertEquals("Une organisation avec cet email existe déjà", exception.getMessage());
verify(organisationRepository).findByEmail("test@lionsclub.org");
verify(organisationRepository, never()).findByNom(anyString());
}
@Test
void testCreerOrganisation_NomDejaExistant() {
// Given
when(organisationRepository.findByEmail(anyString())).thenReturn(Optional.empty());
when(organisationRepository.findByNom(anyString())).thenReturn(Optional.of(organisationTest));
// When & Then
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> organisationService.creerOrganisation(organisationTest));
assertEquals("Une organisation avec ce nom existe déjà", exception.getMessage());
verify(organisationRepository).findByEmail("test@lionsclub.org");
verify(organisationRepository).findByNom("Lions Club Test");
}
@Test
void testMettreAJourOrganisation_Success() {
// Given
Organisation organisationMiseAJour = Organisation.builder()
.nom("Lions Club Test Modifié")
.email("test@lionsclub.org")
.description("Description modifiée")
.telephone("+225 01 02 03 04 06")
.build();
when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.of(organisationTest));
when(organisationRepository.findByNom("Lions Club Test Modifié")).thenReturn(Optional.empty());
// When
Organisation result = organisationService.mettreAJourOrganisation(1L, organisationMiseAJour, "testUser");
// Then
assertNotNull(result);
assertEquals("Lions Club Test Modifié", result.getNom());
assertEquals("Description modifiée", result.getDescription());
assertEquals("+225 01 02 03 04 06", result.getTelephone());
assertEquals("testUser", result.getModifiePar());
assertNotNull(result.getDateModification());
assertEquals(1L, result.getVersion());
}
@Test
void testMettreAJourOrganisation_OrganisationNonTrouvee() {
// Given
when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.empty());
// When & Then
NotFoundException exception = assertThrows(NotFoundException.class,
() -> organisationService.mettreAJourOrganisation(1L, organisationTest, "testUser"));
assertEquals("Organisation non trouvée avec l'ID: 1", exception.getMessage());
}
@Test
void testSupprimerOrganisation_Success() {
// Given
organisationTest.setNombreMembres(0);
when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.of(organisationTest));
// When
organisationService.supprimerOrganisation(1L, "testUser");
// Then
assertFalse(organisationTest.getActif());
assertEquals("DISSOUTE", organisationTest.getStatut());
assertEquals("testUser", organisationTest.getModifiePar());
assertNotNull(organisationTest.getDateModification());
}
@Test
void testSupprimerOrganisation_AvecMembresActifs() {
// Given
organisationTest.setNombreMembres(5);
when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.of(organisationTest));
// When & Then
IllegalStateException exception = assertThrows(IllegalStateException.class,
() -> organisationService.supprimerOrganisation(1L, "testUser"));
assertEquals("Impossible de supprimer une organisation avec des membres actifs", exception.getMessage());
}
@Test
void testTrouverParId_Success() {
// Given
when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.of(organisationTest));
// When
Optional<Organisation> result = organisationService.trouverParId(1L);
// Then
assertTrue(result.isPresent());
assertEquals("Lions Club Test", result.get().getNom());
verify(organisationRepository).findByIdOptional(1L);
}
@Test
void testTrouverParId_NonTrouve() {
// Given
when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.empty());
// When
Optional<Organisation> result = organisationService.trouverParId(1L);
// Then
assertFalse(result.isPresent());
verify(organisationRepository).findByIdOptional(1L);
}
@Test
void testTrouverParEmail_Success() {
// Given
when(organisationRepository.findByEmail("test@lionsclub.org")).thenReturn(Optional.of(organisationTest));
// When
Optional<Organisation> result = organisationService.trouverParEmail("test@lionsclub.org");
// Then
assertTrue(result.isPresent());
assertEquals("Lions Club Test", result.get().getNom());
verify(organisationRepository).findByEmail("test@lionsclub.org");
}
@Test
void testListerOrganisationsActives() {
// Given
List<Organisation> organisations = Arrays.asList(organisationTest);
when(organisationRepository.findAllActives()).thenReturn(organisations);
// When
List<Organisation> result = organisationService.listerOrganisationsActives();
// Then
assertNotNull(result);
assertEquals(1, result.size());
assertEquals("Lions Club Test", result.get(0).getNom());
verify(organisationRepository).findAllActives();
}
@Test
void testActiverOrganisation_Success() {
// Given
organisationTest.setStatut("SUSPENDUE");
organisationTest.setActif(false);
when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.of(organisationTest));
// When
Organisation result = organisationService.activerOrganisation(1L, "testUser");
// Then
assertNotNull(result);
assertEquals("ACTIVE", result.getStatut());
assertTrue(result.getActif());
assertEquals("testUser", result.getModifiePar());
assertNotNull(result.getDateModification());
}
@Test
void testSuspendreOrganisation_Success() {
// Given
when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.of(organisationTest));
// When
Organisation result = organisationService.suspendreOrganisation(1L, "testUser");
// Then
assertNotNull(result);
assertEquals("SUSPENDUE", result.getStatut());
assertFalse(result.getAccepteNouveauxMembres());
assertEquals("testUser", result.getModifiePar());
assertNotNull(result.getDateModification());
}
@Test
void testObtenirStatistiques() {
// Given
when(organisationRepository.count()).thenReturn(100L);
when(organisationRepository.countActives()).thenReturn(85L);
when(organisationRepository.countNouvellesOrganisations(any(LocalDate.class))).thenReturn(5L);
// When
Map<String, Object> result = organisationService.obtenirStatistiques();
// Then
assertNotNull(result);
assertEquals(100L, result.get("totalOrganisations"));
assertEquals(85L, result.get("organisationsActives"));
assertEquals(15L, result.get("organisationsInactives"));
assertEquals(5L, result.get("nouvellesOrganisations30Jours"));
assertEquals(85.0, result.get("tauxActivite"));
assertNotNull(result.get("timestamp"));
}
@Test
void testConvertToDTO() {
// When
var dto = organisationService.convertToDTO(organisationTest);
// Then
assertNotNull(dto);
assertEquals("Lions Club Test", dto.getNom());
assertEquals("LC Test", dto.getNomCourt());
assertEquals("test@lionsclub.org", dto.getEmail());
assertEquals("Organisation de test", dto.getDescription());
assertEquals("+225 01 02 03 04 05", dto.getTelephone());
assertEquals("Abidjan", dto.getVille());
assertEquals(25, dto.getNombreMembres());
assertTrue(dto.getActif());
}
@Test
void testConvertToDTO_Null() {
// When
var dto = organisationService.convertToDTO(null);
// Then
assertNull(dto);
}
@Test
void testConvertFromDTO() {
// Given
var dto = organisationService.convertToDTO(organisationTest);
// When
Organisation result = organisationService.convertFromDTO(dto);
// Then
assertNotNull(result);
assertEquals("Lions Club Test", result.getNom());
assertEquals("LC Test", result.getNomCourt());
assertEquals("test@lionsclub.org", result.getEmail());
assertEquals("Organisation de test", result.getDescription());
assertEquals("+225 01 02 03 04 05", result.getTelephone());
assertEquals("Abidjan", result.getVille());
}
@Test
void testConvertFromDTO_Null() {
// When
Organisation result = organisationService.convertFromDTO(null);
// Then
assertNull(result);
}
}