package dev.lions.unionflow.server.service; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; import dev.lions.unionflow.server.client.UserServiceClient; import dev.lions.unionflow.server.entity.Membre; import dev.lions.unionflow.server.repository.MembreRepository; import dev.lions.user.manager.dto.user.UserDTO; import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO; import dev.lions.user.manager.dto.user.UserSearchResultDTO; import io.quarkus.test.InjectMock; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import jakarta.ws.rs.NotFoundException; import java.util.Collections; import java.util.Optional; import java.util.UUID; import org.eclipse.microprofile.rest.client.inject.RestClient; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @QuarkusTest class MembreKeycloakSyncServiceTest { @Inject MembreKeycloakSyncService syncService; @InjectMock MembreRepository membreRepository; @InjectMock @RestClient UserServiceClient userServiceClient; // ========================================================================= // provisionKeycloakUser // ========================================================================= @Test @DisplayName("provisionKeycloakUser échoue si le membre n'existe pas") void provisionKeycloakUser_failsIfMembreNotFound() { UUID membreId = UUID.randomUUID(); when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.empty()); assertThatThrownBy(() -> syncService.provisionKeycloakUser(membreId)) .isInstanceOf(NotFoundException.class); } @Test @DisplayName("provisionKeycloakUser échoue si le membre a déjà un ID Keycloak") void provisionKeycloakUser_failsIfAlreadyLinked() { UUID membreId = UUID.randomUUID(); Membre membre = new Membre(); membre.setId(membreId); membre.setKeycloakId(UUID.randomUUID()); when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre)); assertThatThrownBy(() -> syncService.provisionKeycloakUser(membreId)) .isInstanceOf(IllegalStateException.class); } @Test @DisplayName("provisionKeycloakUser crée un utilisateur Keycloak et lie le membre") void provisionKeycloakUser_createsAndLinks() { UUID membreId = UUID.randomUUID(); Membre membre = new Membre(); membre.setId(membreId); membre.setEmail("test@unionflow.dev"); membre.setNom("Doe"); membre.setPrenom("John"); when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre)); UserSearchResultDTO searchResult = new UserSearchResultDTO(); searchResult.setUsers(Collections.emptyList()); when(userServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(searchResult); UserDTO createdUser = new UserDTO(); createdUser.setId(UUID.randomUUID().toString()); when(userServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser); syncService.provisionKeycloakUser(membreId); verify(userServiceClient).createUser(any(UserDTO.class), eq("unionflow")); verify(membreRepository).persist(membre); verify(userServiceClient).sendVerificationEmail(eq(createdUser.getId()), eq("unionflow")); } @Test @DisplayName("provisionKeycloakUser lève IllegalStateException si un user Keycloak existe déjà avec cet email") void provisionKeycloakUser_failsIfKeycloakUserAlreadyExists() { UUID membreId = UUID.randomUUID(); Membre membre = new Membre(); membre.setId(membreId); membre.setEmail("duplicate@unionflow.dev"); membre.setNom("Dupont"); membre.setPrenom("Pierre"); when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre)); UserDTO existingUser = new UserDTO(); existingUser.setId(UUID.randomUUID().toString()); existingUser.setEmail("duplicate@unionflow.dev"); UserSearchResultDTO searchResult = new UserSearchResultDTO(); searchResult.setUsers(Collections.singletonList(existingUser)); when(userServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(searchResult); assertThatThrownBy(() -> syncService.provisionKeycloakUser(membreId)) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("duplicate@unionflow.dev"); } @Test @DisplayName("provisionKeycloakUser continue si la recherche Keycloak lève une exception non-ISE") void provisionKeycloakUser_continuesOnSearchException() { UUID membreId = UUID.randomUUID(); Membre membre = new Membre(); membre.setId(membreId); membre.setEmail("search-fail@unionflow.dev"); membre.setNom("Search"); membre.setPrenom("Fail"); when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre)); // search throws a non-ISE exception when(userServiceClient.searchUsers(any())).thenThrow(new RuntimeException("Service unavailable")); UserDTO createdUser = new UserDTO(); createdUser.setId(UUID.randomUUID().toString()); when(userServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser); // Should not throw — it logs warning and continues syncService.provisionKeycloakUser(membreId); verify(userServiceClient).createUser(any(UserDTO.class), eq("unionflow")); } @Test @DisplayName("provisionKeycloakUser tolère un ID Keycloak invalide (non-UUID) dans la réponse") void provisionKeycloakUser_toleratesInvalidKeycloakId() { UUID membreId = UUID.randomUUID(); Membre membre = new Membre(); membre.setId(membreId); membre.setEmail("invalid-id@unionflow.dev"); membre.setNom("Invalid"); membre.setPrenom("Id"); when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre)); UserSearchResultDTO searchResult = new UserSearchResultDTO(); searchResult.setUsers(Collections.emptyList()); when(userServiceClient.searchUsers(any())).thenReturn(searchResult); UserDTO createdUser = new UserDTO(); createdUser.setId("not-a-valid-uuid!!!"); // invalid UUID when(userServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser); // Should not throw — logs warning and persists with null keycloakId syncService.provisionKeycloakUser(membreId); verify(membreRepository).persist(membre); } @Test @DisplayName("provisionKeycloakUser lève RuntimeException si la création Keycloak échoue") void provisionKeycloakUser_throwsRuntimeExceptionOnCreateFailure() { UUID membreId = UUID.randomUUID(); Membre membre = new Membre(); membre.setId(membreId); membre.setEmail("create-fail@unionflow.dev"); membre.setNom("Create"); membre.setPrenom("Fail"); when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre)); UserSearchResultDTO searchResult = new UserSearchResultDTO(); searchResult.setUsers(Collections.emptyList()); when(userServiceClient.searchUsers(any())).thenReturn(searchResult); when(userServiceClient.createUser(any(UserDTO.class), anyString())) .thenThrow(new RuntimeException("Keycloak create failed")); assertThatThrownBy(() -> syncService.provisionKeycloakUser(membreId)) .isInstanceOf(RuntimeException.class) .hasMessageContaining("Impossible de créer le compte Keycloak"); } @Test @DisplayName("provisionKeycloakUser tolère l'échec d'envoi de l'email de vérification") void provisionKeycloakUser_toleratesVerificationEmailFailure() { UUID membreId = UUID.randomUUID(); Membre membre = new Membre(); membre.setId(membreId); membre.setEmail("email-fail@unionflow.dev"); membre.setNom("Email"); membre.setPrenom("Fail"); when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre)); UserSearchResultDTO searchResult = new UserSearchResultDTO(); searchResult.setUsers(Collections.emptyList()); when(userServiceClient.searchUsers(any())).thenReturn(searchResult); UserDTO createdUser = new UserDTO(); createdUser.setId(UUID.randomUUID().toString()); when(userServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser); doThrow(new RuntimeException("SMTP unavailable")) .when(userServiceClient).sendVerificationEmail(anyString(), anyString()); // Should not throw — email failure is non-blocking syncService.provisionKeycloakUser(membreId); 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 // ========================================================================= @Test @DisplayName("syncMembreToKeycloak lève NotFoundException si le membre n'existe pas") void syncMembreToKeycloak_failsIfMembreNotFound() { UUID membreId = UUID.randomUUID(); when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.empty()); assertThatThrownBy(() -> syncService.syncMembreToKeycloak(membreId)) .isInstanceOf(NotFoundException.class); } @Test @DisplayName("syncMembreToKeycloak provisionne automatiquement si pas de compte Keycloak") void syncMembreToKeycloak_provisionesIfNoKeycloakAccount() { UUID membreId = UUID.randomUUID(); Membre membre = new Membre(); membre.setId(membreId); membre.setEmail("no-kc@unionflow.dev"); membre.setNom("No"); membre.setPrenom("KC"); // keycloakId == null → doit appeler provisionKeycloakUser when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre)); UserSearchResultDTO searchResult = new UserSearchResultDTO(); searchResult.setUsers(Collections.emptyList()); when(userServiceClient.searchUsers(any())).thenReturn(searchResult); UserDTO createdUser = new UserDTO(); createdUser.setId(UUID.randomUUID().toString()); when(userServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser); syncService.syncMembreToKeycloak(membreId); // provisionKeycloakUser was invoked internally verify(userServiceClient).createUser(any(UserDTO.class), eq("unionflow")); } @Test @DisplayName("syncMembreToKeycloak met à jour le user Keycloak si le membre est déjà lié") void syncMembreToKeycloak_updatesExistingKeycloakUser() { UUID membreId = UUID.randomUUID(); UUID keycloakId = UUID.randomUUID(); Membre membre = new Membre(); membre.setId(membreId); membre.setKeycloakId(keycloakId); membre.setEmail("sync@unionflow.dev"); membre.setNom("Sync"); membre.setPrenom("Test"); membre.setActif(true); when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre)); UserDTO remoteUser = new UserDTO(); remoteUser.setId(keycloakId.toString()); remoteUser.setRealmName("unionflow"); when(userServiceClient.getUserById(eq(keycloakId.toString()), eq("unionflow"))) .thenReturn(remoteUser); syncService.syncMembreToKeycloak(membreId); verify(userServiceClient).updateUser(eq(keycloakId.toString()), any(UserDTO.class), eq("unionflow")); } @Test @DisplayName("syncMembreToKeycloak lève RuntimeException si l'appel getUserById échoue") void syncMembreToKeycloak_throwsRuntimeExceptionOnGetUserFailure() { UUID membreId = UUID.randomUUID(); UUID keycloakId = UUID.randomUUID(); Membre membre = new Membre(); membre.setId(membreId); membre.setKeycloakId(keycloakId); membre.setEmail("get-fail@unionflow.dev"); membre.setNom("Get"); membre.setPrenom("Fail"); when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre)); when(userServiceClient.getUserById(anyString(), anyString())) .thenThrow(new RuntimeException("Keycloak unreachable")); assertThatThrownBy(() -> syncService.syncMembreToKeycloak(membreId)) .isInstanceOf(RuntimeException.class) .hasMessageContaining("Impossible de synchroniser le user Keycloak"); } @Test @DisplayName("syncMembreToKeycloak utilise actif=true par défaut si membre.actif est null") void syncMembreToKeycloak_usesDefaultEnabledWhenActifIsNull() { UUID membreId = UUID.randomUUID(); UUID keycloakId = UUID.randomUUID(); Membre membre = new Membre(); membre.setId(membreId); membre.setKeycloakId(keycloakId); membre.setEmail("null-actif@unionflow.dev"); membre.setNom("Null"); membre.setPrenom("Actif"); membre.setActif(null); // null → defaults to true when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre)); UserDTO remoteUser = new UserDTO(); remoteUser.setId(keycloakId.toString()); remoteUser.setRealmName("unionflow"); when(userServiceClient.getUserById(anyString(), anyString())).thenReturn(remoteUser); syncService.syncMembreToKeycloak(membreId); verify(userServiceClient).updateUser(anyString(), any(UserDTO.class), anyString()); } // ========================================================================= // syncKeycloakToMembre // ========================================================================= @Test @DisplayName("syncKeycloakToMembre ne fait rien si aucun Membre n'est lié au user Keycloak") void syncKeycloakToMembre_doesNothingIfNoMembreMapped() { String keycloakUserId = UUID.randomUUID().toString(); when(membreRepository.findByKeycloakUserId(keycloakUserId)).thenReturn(Optional.empty()); syncService.syncKeycloakToMembre(keycloakUserId, "unionflow"); verifyNoInteractions(userServiceClient); } @Test @DisplayName("syncKeycloakToMembre met à jour le Membre avec les données Keycloak") void syncKeycloakToMembre_updatesMembreFromKeycloak() { String keycloakUserId = UUID.randomUUID().toString(); Membre membre = new Membre(); membre.setId(UUID.randomUUID()); membre.setEmail("old@unionflow.dev"); membre.setNom("OldNom"); membre.setPrenom("OldPrenom"); when(membreRepository.findByKeycloakUserId(keycloakUserId)).thenReturn(Optional.of(membre)); UserDTO keycloakUser = new UserDTO(); keycloakUser.setId(keycloakUserId); keycloakUser.setPrenom("NewPrenom"); keycloakUser.setNom("NewNom"); keycloakUser.setEmail("new@unionflow.dev"); keycloakUser.setEnabled(false); when(userServiceClient.getUserById(eq(keycloakUserId), eq("unionflow"))).thenReturn(keycloakUser); syncService.syncKeycloakToMembre(keycloakUserId, "unionflow"); assertThat(membre.getPrenom()).isEqualTo("NewPrenom"); assertThat(membre.getNom()).isEqualTo("NewNom"); assertThat(membre.getEmail()).isEqualTo("new@unionflow.dev"); assertThat(membre.getActif()).isFalse(); verify(membreRepository).persist(membre); } @Test @DisplayName("syncKeycloakToMembre utilise DEFAULT_REALM si realm est null") void syncKeycloakToMembre_usesDefaultRealmWhenNull() { String keycloakUserId = UUID.randomUUID().toString(); Membre membre = new Membre(); membre.setId(UUID.randomUUID()); when(membreRepository.findByKeycloakUserId(keycloakUserId)).thenReturn(Optional.of(membre)); UserDTO keycloakUser = new UserDTO(); keycloakUser.setId(keycloakUserId); keycloakUser.setPrenom("P"); keycloakUser.setNom("N"); keycloakUser.setEmail("e@e.com"); keycloakUser.setEnabled(true); when(userServiceClient.getUserById(eq(keycloakUserId), eq("unionflow"))).thenReturn(keycloakUser); syncService.syncKeycloakToMembre(keycloakUserId, null); // null realm verify(userServiceClient).getUserById(eq(keycloakUserId), eq("unionflow")); } @Test @DisplayName("syncKeycloakToMembre lève RuntimeException si getUserById échoue") void syncKeycloakToMembre_throwsRuntimeExceptionOnFailure() { String keycloakUserId = UUID.randomUUID().toString(); Membre membre = new Membre(); membre.setId(UUID.randomUUID()); when(membreRepository.findByKeycloakUserId(keycloakUserId)).thenReturn(Optional.of(membre)); when(userServiceClient.getUserById(anyString(), anyString())) .thenThrow(new RuntimeException("Timeout")); assertThatThrownBy(() -> syncService.syncKeycloakToMembre(keycloakUserId, "unionflow")) .isInstanceOf(RuntimeException.class) .hasMessageContaining("Impossible de synchroniser depuis Keycloak"); } // ========================================================================= // findMembreByKeycloakUserId // ========================================================================= @Test @DisplayName("findMembreByKeycloakUserId retourne Optional.empty() si aucun membre n'est lié") void findMembreByKeycloakUserId_returnsEmptyIfNotFound() { String keycloakUserId = UUID.randomUUID().toString(); when(membreRepository.findByKeycloakUserId(keycloakUserId)).thenReturn(Optional.empty()); Optional result = syncService.findMembreByKeycloakUserId(keycloakUserId); assertThat(result).isEmpty(); } @Test @DisplayName("findMembreByKeycloakUserId retourne le Membre lié si trouvé") void findMembreByKeycloakUserId_returnsMembre() { String keycloakUserId = UUID.randomUUID().toString(); Membre membre = new Membre(); membre.setId(UUID.randomUUID()); membre.setEmail("linked@unionflow.dev"); when(membreRepository.findByKeycloakUserId(keycloakUserId)).thenReturn(Optional.of(membre)); Optional result = syncService.findMembreByKeycloakUserId(keycloakUserId); assertThat(result).isPresent(); assertThat(result.get()).isSameAs(membre); } // ========================================================================= // unlinkKeycloakUser // ========================================================================= @Test @DisplayName("unlinkKeycloakUser lève NotFoundException si le membre n'existe pas") void unlinkKeycloakUser_failsIfMembreNotFound() { UUID membreId = UUID.randomUUID(); when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.empty()); assertThatThrownBy(() -> syncService.unlinkKeycloakUser(membreId)) .isInstanceOf(NotFoundException.class); } // ========================================================================= // provisionKeycloakUser — branche searchResult == null (L97 branche null) // ========================================================================= @Test @DisplayName("provisionKeycloakUser avec searchResult==null depuis API → continue (branche null L97)") void provisionKeycloakUser_searchResultNull_continues() { UUID membreId = UUID.randomUUID(); Membre membre = new Membre(); membre.setId(membreId); membre.setNom("Null"); membre.setPrenom("Search"); membre.setEmail("null-search-" + membreId + "@test.com"); membre.setKeycloakId(null); when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre)); // searchResult == null → condition L97 = false → continue when(userServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(null); // provisionKeycloakUser tente ensuite de créer l'utilisateur via createUser // On mock createUser pour qu'il retourne un UserDTO valide UserDTO createdUser = new UserDTO(); createdUser.setId(UUID.randomUUID().toString()); createdUser.setUsername(membre.getEmail()); when(userServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser); // Ne doit pas lever d'exception (searchResult null → condition false → continue) org.assertj.core.api.Assertions.assertThatCode(() -> syncService.provisionKeycloakUser(membreId)) .doesNotThrowAnyException(); } @Test @DisplayName("provisionKeycloakUser avec searchResult.getUsers()==null → continue (branche getUsers()==null L97)") void provisionKeycloakUser_usersNull_continues() { UUID membreId = UUID.randomUUID(); Membre membre = new Membre(); membre.setId(membreId); membre.setNom("UsersNull"); membre.setPrenom("Test"); membre.setEmail("users-null-" + membreId + "@test.com"); membre.setKeycloakId(null); when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre)); // searchResult non-null mais users == null → condition L97 = false → continue UserSearchResultDTO searchResult = new UserSearchResultDTO(); searchResult.setUsers(null); when(userServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(searchResult); UserDTO createdUser = new UserDTO(); createdUser.setId(UUID.randomUUID().toString()); createdUser.setUsername(membre.getEmail()); when(userServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser); org.assertj.core.api.Assertions.assertThatCode(() -> syncService.provisionKeycloakUser(membreId)) .doesNotThrowAnyException(); } // ========================================================================= // L117: createdUser.getId() == null → false branch (skip setKeycloakId) // ========================================================================= @Test @DisplayName("provisionKeycloakUser avec createdUser.getId() null → L117 false → keycloakId non settée") void provisionKeycloakUser_createdUserIdNull_L117False_keycloakIdNotSet() { UUID membreId = UUID.randomUUID(); Membre membre = new Membre(); membre.setId(membreId); membre.setNom("IdNull"); membre.setPrenom("Test"); membre.setEmail("id-null-" + membreId + "@test.com"); membre.setKeycloakId(null); when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre)); // searchResult null ou sans users → membre not found → create UserSearchResultDTO searchResult = new UserSearchResultDTO(); searchResult.setUsers(null); when(userServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(searchResult); // createdUser with getId() = null → L117: null != null = false → skip setKeycloakId UserDTO createdUser = new UserDTO(); createdUser.setId(null); // null → L117 false branch when(userServiceClient.createUser(any(UserDTO.class), anyString())).thenReturn(createdUser); org.assertj.core.api.Assertions.assertThatCode(() -> syncService.provisionKeycloakUser(membreId)) .doesNotThrowAnyException(); // keycloakId should remain null since getId() was null assertThat(membre.getKeycloakId()).isNull(); } @Test @DisplayName("unlinkKeycloakUser ne fait rien si le membre n'a pas de compte Keycloak lié") void unlinkKeycloakUser_doesNothingIfNoKeycloakId() { UUID membreId = UUID.randomUUID(); Membre membre = new Membre(); membre.setId(membreId); membre.setKeycloakId(null); when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre)); syncService.unlinkKeycloakUser(membreId); // persist should not be called when there is nothing to unlink verify(membreRepository, never()).persist(any(Membre.class)); } @Test @DisplayName("unlinkKeycloakUser supprime le lien Keycloak et persiste le membre") void unlinkKeycloakUser_removesKeycloakLink() { UUID membreId = UUID.randomUUID(); UUID keycloakId = UUID.randomUUID(); Membre membre = new Membre(); membre.setId(membreId); membre.setKeycloakId(keycloakId); membre.setNom("Unlink"); membre.setPrenom("Test"); when(membreRepository.findByIdOptional(membreId)).thenReturn(Optional.of(membre)); syncService.unlinkKeycloakUser(membreId); assertThat(membre.getKeycloakId()).isNull(); verify(membreRepository).persist(membre); } }