Refactoring - Version stable
This commit is contained in:
@@ -674,6 +674,33 @@ public class MembreResource {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("/{id}/promouvoir-admin-organisation")
|
||||||
|
@RolesAllowed({"ADMIN", "SUPER_ADMIN"})
|
||||||
|
@Operation(
|
||||||
|
summary = "Promouvoir un membre en administrateur d'organisation",
|
||||||
|
description = "Passe le membre en statut ACTIF et lui assigne le rôle ADMIN_ORGANISATION dans Keycloak. "
|
||||||
|
+ "Réservé aux super administrateurs de la plateforme. "
|
||||||
|
+ "Le compte est immédiatement opérationnel sans validation intermédiaire.")
|
||||||
|
@APIResponse(responseCode = "200", description = "Membre promu administrateur d'organisation")
|
||||||
|
@APIResponse(responseCode = "404", description = "Membre non trouvé")
|
||||||
|
@APIResponse(responseCode = "403", description = "Accès réservé aux ADMIN / SUPER_ADMIN")
|
||||||
|
public Response promouvoirAdminOrganisation(
|
||||||
|
@Parameter(description = "UUID du membre à promouvoir") @PathParam("id") UUID id) {
|
||||||
|
LOG.infof("Promotion admin d'organisation pour le membre ID: %s", id);
|
||||||
|
|
||||||
|
Membre membrePromu = membreService.promouvoirAdminOrganisation(id);
|
||||||
|
|
||||||
|
// Assigner ADMIN_ORGANISATION dans Keycloak (non bloquant)
|
||||||
|
try {
|
||||||
|
keycloakSyncService.promouvoirAdminOrganisationDansKeycloak(id);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.warnf("Promotion Keycloak échouée pour %s (non bloquant): %s", membrePromu.getEmail(), e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.ok(membreService.convertToResponse(membrePromu)).build();
|
||||||
|
}
|
||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
@Path("/{id}/activer")
|
@Path("/{id}/activer")
|
||||||
@RolesAllowed({"ADMIN", "SUPER_ADMIN"})
|
@RolesAllowed({"ADMIN", "SUPER_ADMIN"})
|
||||||
|
|||||||
@@ -196,6 +196,62 @@ public class MembreKeycloakSyncService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promeut un membre au rôle ADMIN_ORGANISATION dans Keycloak.
|
||||||
|
* Appelé après MembreService.promouvoirAdminOrganisation().
|
||||||
|
*
|
||||||
|
* <p>Si le membre n'a pas encore de compte Keycloak, le provisionne d'abord.
|
||||||
|
* Assigne ensuite ADMIN_ORGANISATION et retire les rôles MEMBRE / MEMBRE_ACTIF
|
||||||
|
* (un admin gère l'organisation — il n'est pas un membre ordinaire).
|
||||||
|
*
|
||||||
|
* @param membreId UUID du membre à promouvoir dans Keycloak
|
||||||
|
* @throws NotFoundException si le membre n'existe pas en base
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void promouvoirAdminOrganisationDansKeycloak(java.util.UUID membreId) {
|
||||||
|
LOGGER.info("Promotion Keycloak (rôle ADMIN_ORGANISATION) pour Membre ID: " + membreId);
|
||||||
|
|
||||||
|
Membre membre = membreRepository.findByIdOptional(membreId)
|
||||||
|
.orElseThrow(() -> new NotFoundException("Membre non trouvé avec l'ID: " + membreId));
|
||||||
|
|
||||||
|
// Provisionner le compte Keycloak s'il n'existe pas encore
|
||||||
|
if (membre.getKeycloakId() == null) {
|
||||||
|
LOGGER.info("Compte Keycloak absent — provisionnement automatique pour " + membre.getNomComplet());
|
||||||
|
provisionKeycloakUser(membreId);
|
||||||
|
membre = membreRepository.findByIdOptional(membreId)
|
||||||
|
.orElseThrow(() -> new NotFoundException("Membre non trouvé après provisionnement: " + membreId));
|
||||||
|
}
|
||||||
|
|
||||||
|
String keycloakUserId = membre.getKeycloakId().toString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
UserDTO user = userServiceClient.getUserById(keycloakUserId, DEFAULT_REALM);
|
||||||
|
|
||||||
|
// S'assurer que le compte est activé
|
||||||
|
if (Boolean.FALSE.equals(user.getEnabled())) {
|
||||||
|
user.setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construire la liste de rôles : ajouter ADMIN_ORGANISATION, retirer MEMBRE et MEMBRE_ACTIF
|
||||||
|
List<String> roles = user.getRealmRoles() != null
|
||||||
|
? new java.util.ArrayList<>(user.getRealmRoles())
|
||||||
|
: new java.util.ArrayList<>();
|
||||||
|
roles.remove("MEMBRE");
|
||||||
|
roles.remove("MEMBRE_ACTIF");
|
||||||
|
if (!roles.contains("ADMIN_ORGANISATION")) {
|
||||||
|
roles.add("ADMIN_ORGANISATION");
|
||||||
|
}
|
||||||
|
user.setRealmRoles(roles);
|
||||||
|
userServiceClient.updateUser(keycloakUserId, user, DEFAULT_REALM);
|
||||||
|
|
||||||
|
LOGGER.info("✅ Rôle ADMIN_ORGANISATION assigné dans Keycloak pour " + membre.getNomComplet());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.severe("❌ Erreur promotion Keycloak pour " + membre.getNomComplet() + ": " + e.getMessage());
|
||||||
|
throw new RuntimeException("Impossible de promouvoir le compte Keycloak: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronise les données du Membre vers le User Keycloak.
|
* Synchronise les données du Membre vers le User Keycloak.
|
||||||
* Si le membre n'a pas de compte Keycloak, le provisionne automatiquement.
|
* Si le membre n'a pas de compte Keycloak, le provisionne automatiquement.
|
||||||
|
|||||||
@@ -118,6 +118,33 @@ public class MembreService {
|
|||||||
return membre;
|
return membre;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promeut un membre au rôle d'administrateur d'organisation.
|
||||||
|
* Passe immédiatement le statut à ACTIF — les admins sont opérationnels sans
|
||||||
|
* validation intermédiaire.
|
||||||
|
* Doit être suivi d'un appel à
|
||||||
|
* MembreKeycloakSyncService.promouvoirAdminOrganisationDansKeycloak()
|
||||||
|
* pour que le rôle ADMIN_ORGANISATION soit assigné dans Keycloak.
|
||||||
|
*
|
||||||
|
* @param membreId UUID du membre à promouvoir
|
||||||
|
* @return Le membre mis à jour
|
||||||
|
* @throws jakarta.ws.rs.NotFoundException si le membre est introuvable
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Membre promouvoirAdminOrganisation(UUID membreId) {
|
||||||
|
LOG.infof("Promotion admin d'organisation pour le membre ID: %s", membreId);
|
||||||
|
|
||||||
|
Membre membre = membreRepository.findByIdOptional(membreId)
|
||||||
|
.orElseThrow(() -> new jakarta.ws.rs.NotFoundException("Membre non trouvé avec l'ID: " + membreId));
|
||||||
|
|
||||||
|
membre.setStatutCompte("ACTIF");
|
||||||
|
membre.setActif(true);
|
||||||
|
membreRepository.persist(membre);
|
||||||
|
|
||||||
|
LOG.infof("Membre promu admin d'organisation: %s (ID: %s)", membre.getNomComplet(), membreId);
|
||||||
|
return membre;
|
||||||
|
}
|
||||||
|
|
||||||
/** Met à jour un membre existant */
|
/** Met à jour un membre existant */
|
||||||
@Transactional
|
@Transactional
|
||||||
public Membre mettreAJourMembre(UUID id, Membre membreModifie) {
|
public Membre mettreAJourMembre(UUID id, Membre membreModifie) {
|
||||||
|
|||||||
@@ -217,6 +217,148 @@ class MembreKeycloakSyncServiceTest {
|
|||||||
verify(membreRepository).persist(membre);
|
verify(membreRepository).persist(membre);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// promouvoirAdminOrganisationDansKeycloak
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("promouvoirAdminOrganisation échoue si le membre n'existe pas")
|
||||||
|
void promouvoirAdminOrganisation_failsIfMembreNotFound() {
|
||||||
|
UUID membreId = UUID.randomUUID();
|
||||||
|
when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> syncService.promouvoirAdminOrganisationDansKeycloak(membreId))
|
||||||
|
.isInstanceOf(NotFoundException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("promouvoirAdminOrganisation assigne ADMIN_ORGANISATION et retire MEMBRE/MEMBRE_ACTIF")
|
||||||
|
void promouvoirAdminOrganisation_assignsAdminRoleAndRemovesMemberRoles() {
|
||||||
|
UUID membreId = UUID.randomUUID();
|
||||||
|
UUID keycloakId = UUID.randomUUID();
|
||||||
|
|
||||||
|
Membre membre = new Membre();
|
||||||
|
membre.setId(membreId);
|
||||||
|
membre.setKeycloakId(keycloakId);
|
||||||
|
membre.setEmail("admin@unionflow.dev");
|
||||||
|
membre.setNom("Admin");
|
||||||
|
membre.setPrenom("New");
|
||||||
|
|
||||||
|
when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));
|
||||||
|
|
||||||
|
UserDTO user = new UserDTO();
|
||||||
|
user.setId(keycloakId.toString());
|
||||||
|
user.setEnabled(true);
|
||||||
|
user.setRealmRoles(new java.util.ArrayList<>(java.util.List.of("MEMBRE", "MEMBRE_ACTIF")));
|
||||||
|
user.setRealmName("unionflow");
|
||||||
|
when(userServiceClient.getUserById(eq(keycloakId.toString()), eq("unionflow"))).thenReturn(user);
|
||||||
|
when(userServiceClient.updateUser(anyString(), any(UserDTO.class), anyString())).thenReturn(user);
|
||||||
|
|
||||||
|
syncService.promouvoirAdminOrganisationDansKeycloak(membreId);
|
||||||
|
|
||||||
|
verify(userServiceClient).updateUser(eq(keycloakId.toString()), any(UserDTO.class), eq("unionflow"));
|
||||||
|
assertThat(user.getRealmRoles()).contains("ADMIN_ORGANISATION");
|
||||||
|
assertThat(user.getRealmRoles()).doesNotContain("MEMBRE", "MEMBRE_ACTIF");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("promouvoirAdminOrganisation active le compte s'il est désactivé dans Keycloak")
|
||||||
|
void promouvoirAdminOrganisation_enablesDisabledAccount() {
|
||||||
|
UUID membreId = UUID.randomUUID();
|
||||||
|
UUID keycloakId = UUID.randomUUID();
|
||||||
|
|
||||||
|
Membre membre = new Membre();
|
||||||
|
membre.setId(membreId);
|
||||||
|
membre.setKeycloakId(keycloakId);
|
||||||
|
membre.setEmail("disabled-admin@unionflow.dev");
|
||||||
|
membre.setNom("Disabled");
|
||||||
|
membre.setPrenom("Admin");
|
||||||
|
|
||||||
|
when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));
|
||||||
|
|
||||||
|
UserDTO user = new UserDTO();
|
||||||
|
user.setId(keycloakId.toString());
|
||||||
|
user.setEnabled(false); // désactivé
|
||||||
|
user.setRealmRoles(new java.util.ArrayList<>());
|
||||||
|
user.setRealmName("unionflow");
|
||||||
|
when(userServiceClient.getUserById(anyString(), anyString())).thenReturn(user);
|
||||||
|
when(userServiceClient.updateUser(anyString(), any(UserDTO.class), anyString())).thenReturn(user);
|
||||||
|
|
||||||
|
syncService.promouvoirAdminOrganisationDansKeycloak(membreId);
|
||||||
|
|
||||||
|
assertThat(user.getEnabled()).isTrue();
|
||||||
|
verify(userServiceClient).updateUser(anyString(), any(UserDTO.class), anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("promouvoirAdminOrganisation provisionne Keycloak si keycloakId est null")
|
||||||
|
void promouvoirAdminOrganisation_provisionesIfNoKeycloakAccount() {
|
||||||
|
UUID membreId = UUID.randomUUID();
|
||||||
|
|
||||||
|
Membre membre = new Membre();
|
||||||
|
membre.setId(membreId);
|
||||||
|
membre.setEmail("no-kc-admin@unionflow.dev");
|
||||||
|
membre.setNom("No");
|
||||||
|
membre.setPrenom("KC");
|
||||||
|
// keycloakId == null
|
||||||
|
|
||||||
|
UUID newKeycloakId = UUID.randomUUID();
|
||||||
|
Membre membreWithKc = new Membre();
|
||||||
|
membreWithKc.setId(membreId);
|
||||||
|
membreWithKc.setEmail("no-kc-admin@unionflow.dev");
|
||||||
|
membreWithKc.setNom("No");
|
||||||
|
membreWithKc.setPrenom("KC");
|
||||||
|
membreWithKc.setKeycloakId(newKeycloakId);
|
||||||
|
|
||||||
|
when(membreRepository.findByIdOptional(membreId))
|
||||||
|
.thenReturn(Optional.of(membre)) // 1er appel (promouvoir) : pas de keycloakId → déclenche provisionnement
|
||||||
|
.thenReturn(Optional.of(membre)) // 2e appel (provisionKeycloakUser interne) : pas de keycloakId
|
||||||
|
.thenReturn(Optional.of(membreWithKc)); // 3e appel : rechargement après provisionnement
|
||||||
|
|
||||||
|
UserSearchResultDTO searchResult = new UserSearchResultDTO();
|
||||||
|
searchResult.setUsers(java.util.Collections.emptyList());
|
||||||
|
when(userServiceClient.searchUsers(any())).thenReturn(searchResult);
|
||||||
|
|
||||||
|
UserDTO createdUser = new UserDTO();
|
||||||
|
createdUser.setId(newKeycloakId.toString());
|
||||||
|
when(userServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser);
|
||||||
|
|
||||||
|
UserDTO fetchedUser = new UserDTO();
|
||||||
|
fetchedUser.setId(newKeycloakId.toString());
|
||||||
|
fetchedUser.setEnabled(true);
|
||||||
|
fetchedUser.setRealmRoles(new java.util.ArrayList<>());
|
||||||
|
fetchedUser.setRealmName("unionflow");
|
||||||
|
when(userServiceClient.getUserById(eq(newKeycloakId.toString()), anyString())).thenReturn(fetchedUser);
|
||||||
|
when(userServiceClient.updateUser(anyString(), any(UserDTO.class), anyString())).thenReturn(fetchedUser);
|
||||||
|
|
||||||
|
syncService.promouvoirAdminOrganisationDansKeycloak(membreId);
|
||||||
|
|
||||||
|
verify(userServiceClient).createUser(any(UserDTO.class), eq("unionflow"));
|
||||||
|
verify(userServiceClient).updateUser(eq(newKeycloakId.toString()), any(UserDTO.class), eq("unionflow"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("promouvoirAdminOrganisation lève RuntimeException si l'appel Keycloak échoue")
|
||||||
|
void promouvoirAdminOrganisation_throwsRuntimeExceptionOnKeycloakFailure() {
|
||||||
|
UUID membreId = UUID.randomUUID();
|
||||||
|
UUID keycloakId = UUID.randomUUID();
|
||||||
|
|
||||||
|
Membre membre = new Membre();
|
||||||
|
membre.setId(membreId);
|
||||||
|
membre.setKeycloakId(keycloakId);
|
||||||
|
membre.setEmail("fail-admin@unionflow.dev");
|
||||||
|
membre.setNom("Fail");
|
||||||
|
membre.setPrenom("Admin");
|
||||||
|
|
||||||
|
when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre));
|
||||||
|
when(userServiceClient.getUserById(anyString(), anyString()))
|
||||||
|
.thenThrow(new RuntimeException("Keycloak unreachable"));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> syncService.promouvoirAdminOrganisationDansKeycloak(membreId))
|
||||||
|
.isInstanceOf(RuntimeException.class)
|
||||||
|
.hasMessageContaining("Impossible de promouvoir le compte Keycloak");
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// syncMembreToKeycloak
|
// syncMembreToKeycloak
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -246,6 +246,50 @@ class MembreServiceTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// promouvoirAdminOrganisation
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("promouvoirAdminOrganisation")
|
||||||
|
class PromouvoirAdminOrganisationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Happy path: membre promu → ACTIF + actif=true immédiatement")
|
||||||
|
void promouvoirAdminOrganisation_setsActifImmediately() {
|
||||||
|
UUID id = UUID.randomUUID();
|
||||||
|
Membre membre = new Membre();
|
||||||
|
membre.setId(id);
|
||||||
|
membre.setEmail("future-admin@unionflow.dev");
|
||||||
|
membre.setNom("Admin");
|
||||||
|
membre.setPrenom("New");
|
||||||
|
membre.setStatutCompte("EN_ATTENTE_VALIDATION");
|
||||||
|
membre.setActif(false);
|
||||||
|
|
||||||
|
when(membreRepository.findByIdOptional(id)).thenReturn(Optional.of(membre));
|
||||||
|
doNothing().when(membreRepository).persist(any(Membre.class));
|
||||||
|
|
||||||
|
Membre result = membreService.promouvoirAdminOrganisation(id);
|
||||||
|
|
||||||
|
assertThat(result.getStatutCompte()).isEqualTo("ACTIF");
|
||||||
|
assertThat(result.getActif()).isTrue();
|
||||||
|
verify(membreRepository).persist(membre);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Membre introuvable: lève NotFoundException")
|
||||||
|
void promouvoirAdminOrganisation_notFound_throws() {
|
||||||
|
UUID id = UUID.randomUUID();
|
||||||
|
when(membreRepository.findByIdOptional(id)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> membreService.promouvoirAdminOrganisation(id))
|
||||||
|
.isInstanceOf(jakarta.ws.rs.NotFoundException.class)
|
||||||
|
.hasMessageContaining("non trouvé");
|
||||||
|
|
||||||
|
verify(membreRepository, never()).persist(any(Membre.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// mettreAJourMembre
|
// mettreAJourMembre
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user