diff --git a/src/main/java/de/itvsh/kop/user/RemoteUserIterator.java b/src/main/java/de/itvsh/kop/user/RemoteUserIterator.java new file mode 100644 index 0000000000000000000000000000000000000000..93958622763c86fb9c693f5316876d93434382e8 --- /dev/null +++ b/src/main/java/de/itvsh/kop/user/RemoteUserIterator.java @@ -0,0 +1,47 @@ +package de.itvsh.kop.user; + +import java.util.Iterator; + +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.representations.idm.UserRepresentation; + +import com.cronutils.utils.StringUtils; + +public class RemoteUserIterator implements Iterator<UserRepresentation> { + + private static final int PAGE_SIZE = 10;// TODO + + private final UsersResource users; + private final int count; + + private int step = 0; + private Iterator<UserRepresentation> loadedUserIterator; + + public RemoteUserIterator(RealmResource resource) { + users = resource.users(); + count = users.count(); + loadNextStep(); + } + + @Override + public boolean hasNext() { + if (loadedUserIterator.hasNext()) { + return true; + } + if (count > step * PAGE_SIZE) { + loadNextStep(); + return loadedUserIterator.hasNext(); + } + return false; + } + + private void loadNextStep() { + loadedUserIterator = users.search(StringUtils.EMPTY, step * PAGE_SIZE, ++step * PAGE_SIZE).iterator(); + } + + @Override + public UserRepresentation next() { + return loadedUserIterator.next(); + } +} \ No newline at end of file diff --git a/src/main/java/de/itvsh/kop/user/keycloak/KeycloakApiService.java b/src/main/java/de/itvsh/kop/user/keycloak/KeycloakApiService.java index ac6b1b8f70af2db96c4135da8a6593ab1385cc62..03dec638d5300f86b37f4ad8b92e7d3eb85543f2 100644 --- a/src/main/java/de/itvsh/kop/user/keycloak/KeycloakApiService.java +++ b/src/main/java/de/itvsh/kop/user/keycloak/KeycloakApiService.java @@ -1,7 +1,9 @@ package de.itvsh.kop.user.keycloak; +import java.util.Spliterators; import java.util.function.Supplier; import java.util.stream.Stream; +import java.util.stream.StreamSupport; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; @@ -10,7 +12,9 @@ import javax.ws.rs.ProcessingException; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.representations.idm.UserRepresentation; +import de.itvsh.kop.user.RemoteUserIterator; import de.itvsh.kop.user.User; import de.itvsh.kop.user.UserResourceMapper; import de.itvsh.kop.user.common.errorhandling.KeycloakClientException; @@ -30,10 +34,20 @@ class KeycloakApiService { private String keycloakUrl; public Stream<User> findAllUser() { - return handlingKeycloakException(() -> { - var users = realmResource.users(); - return users.list().stream().map(userRep -> mapper.toKopUser(users.get(userRep.getId()))); - }); + return handlingKeycloakException(() -> getAllUserRepresentation().map(this::toUser)); + } + + Stream<UserRepresentation> getAllUserRepresentation() { + var iterator = createRealmUserIterator(); + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false); + } + + private RemoteUserIterator createRealmUserIterator() { + return new RemoteUserIterator(realmResource); + } + + private User toUser(UserRepresentation userRepresentation) { + return mapper.toKopUser(realmResource.users().get(userRepresentation.getId())); } private <T> T handlingKeycloakException(Supplier<T> runnable) { diff --git a/src/test/java/de/itvsh/kop/user/UserRepresentationTestFactory.java b/src/test/java/de/itvsh/kop/user/UserRepresentationTestFactory.java index 55c73a340213a084e7284b4a0974f234b2683f2e..9f95fd80c0591d15be7066d2904f88dea6f35c1b 100644 --- a/src/test/java/de/itvsh/kop/user/UserRepresentationTestFactory.java +++ b/src/test/java/de/itvsh/kop/user/UserRepresentationTestFactory.java @@ -8,7 +8,7 @@ import org.keycloak.representations.idm.UserRepresentation; import com.github.javafaker.Faker; -class UserRepresentationTestFactory { +public class UserRepresentationTestFactory { static final String EMAIL = Faker.instance().internet().emailAddress(); static final String FIRST_NAME = Faker.instance().name().firstName(); static final String LAST_NAME = Faker.instance().name().lastName(); @@ -21,7 +21,7 @@ class UserRepresentationTestFactory { static final Map<String, List<String>> ATTRIBUTES = Map.of("LDAP_ID", List.of(EXTERNAL_ID), "organisationseinheitId", List.of(ORGANSISATIONS_EINHEIT_ID)); - static UserRepresentation create() { + public static UserRepresentation create() { UserRepresentation user = new UserRepresentation(); user.setEmail(EMAIL); user.setFirstName(FIRST_NAME); diff --git a/src/test/java/de/itvsh/kop/user/keycloak/KeycloakApiServiceITCase.java b/src/test/java/de/itvsh/kop/user/keycloak/KeycloakApiServiceITCase.java index 04ad30ed9aef5960a9ac0cc526c57c43a58ff9d2..7dc6275081eacc25859c1887b38d75cba2573093 100644 --- a/src/test/java/de/itvsh/kop/user/keycloak/KeycloakApiServiceITCase.java +++ b/src/test/java/de/itvsh/kop/user/keycloak/KeycloakApiServiceITCase.java @@ -20,6 +20,6 @@ class KeycloakApiServiceITCase { void shouldGetAllUsers() { var usersStream = service.findAllUser().toList(); - assertThat(usersStream).isNotNull(); + assertThat(usersStream).isNotEmpty(); } } \ No newline at end of file diff --git a/src/test/java/de/itvsh/kop/user/keycloak/KeycloakApiServiceTest.java b/src/test/java/de/itvsh/kop/user/keycloak/KeycloakApiServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..967f3f745e7f1699c1b0658e3ec14f4b7c135e10 --- /dev/null +++ b/src/test/java/de/itvsh/kop/user/keycloak/KeycloakApiServiceTest.java @@ -0,0 +1,78 @@ +package de.itvsh.kop.user.keycloak; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.representations.idm.UserRepresentation; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +import de.itvsh.kop.user.RemoteUserIterator; +import de.itvsh.kop.user.User; +import de.itvsh.kop.user.UserRepresentationTestFactory; +import de.itvsh.kop.user.UserResourceMapper; +import de.itvsh.kop.user.UserTestFactory; + +class KeycloakApiServiceTest { + + @Spy + @InjectMocks + private KeycloakApiService service; + @Mock + private RealmResource realmResource; + @Mock + private UserResourceMapper userResourceMapper; + @Mock + private KeycloakApiProperties properties; + + @Mock + private UserResource userResource; + + @DisplayName("Find all user") + @Nested + class TestFindAllUser { + + @Mock + private UsersResource usersResource; + + @Mock + private RemoteUserIterator iterator; + + private User user = UserTestFactory.create(); + private UserRepresentation userRepresentation = UserRepresentationTestFactory.create(); + private Stream<UserRepresentation> userStream = Stream.of(userRepresentation); + + @BeforeEach + void init() { + doReturn(userStream).when(service).getAllUserRepresentation(); + } + + @Test + void shouldCreateIteratorWithRealmResource() { + service.findAllUser(); + + verify(service).getAllUserRepresentation(); + } + + @Test + void shouldCallMapper() { + when(userResourceMapper.toKopUser(any(UserResource.class))).thenReturn(user); + when(realmResource.users()).thenReturn(usersResource); + when(usersResource.get(anyString())).thenReturn(userResource); + + service.findAllUser().toList(); + + verify(userResourceMapper).toKopUser(any(UserResource.class)); + } + } +} \ No newline at end of file