Repository: gravitee-io/graviteeio-access-management
Pull request #690 (2019-12-07 14:08:55)

Fix users search pagination in Management API
This PR fixes issue https://github.com/gravitee-io/issues/issues/2994 by taking into account the page parameter in the users list endpoint of the Management API not only for users listing but also for searching (when parameter `q` is passed).

The actual fix is in the MongoDB query: call to `skip()` was missing in class `MongoDbRepository`. The rest are simply adaptations of methods signatures, in order to pass the `page` parameter from the endpoint to the query.

Added also a test case to check the correctness of paginated results.

ventuc: Please note that for file UsersResource only line 85 is really changed, the of the file is a before... don't know why git is showing that all the lines have been changed. For my IDE only line 85 is changed.

Patch:
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UsersResource.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UsersResource.java
index 0dafc9b934..a53031278b 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UsersResource.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/UsersResource.java
@@ -1,157 +1,157 @@
-/**
- * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *         http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package io.gravitee.am.management.handlers.management.api.resources;
-
-import io.gravitee.am.management.service.UserService;
-import io.gravitee.am.model.User;
-import io.gravitee.am.model.common.Page;
-import io.gravitee.am.service.DomainService;
-import io.gravitee.am.service.IdentityProviderService;
-import io.gravitee.am.service.authentication.crypto.password.PasswordValidator;
-import io.gravitee.am.service.exception.DomainNotFoundException;
-import io.gravitee.am.service.exception.UserInvalidException;
-import io.gravitee.am.service.model.NewUser;
-import io.gravitee.common.http.MediaType;
-import io.reactivex.Maybe;
-import io.reactivex.Observable;
-import io.reactivex.Single;
-import io.swagger.annotations.*;
-import org.springframework.beans.factory.annotation.Autowired;
-
-import javax.validation.Valid;
-import javax.validation.constraints.NotNull;
-import javax.ws.rs.*;
-import javax.ws.rs.container.AsyncResponse;
-import javax.ws.rs.container.ResourceContext;
-import javax.ws.rs.container.Suspended;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.Response;
-import java.net.URI;
-import java.util.Comparator;
-
-/**
- * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
- * @author GraviteeSource Team
- */
-@Api(tags = {"user"})
-public class UsersResource extends AbstractResource {
-
-    private static final int MAX_USERS_SIZE_PER_PAGE = 30;
-    private static final String MAX_USERS_SIZE_PER_PAGE_STRING = "30";
-
-    @Context
-    private ResourceContext resourceContext;
-
-    @Autowired
-    private UserService userService;
-
-    @Autowired
-    private DomainService domainService;
-
-    @Autowired
-    private IdentityProviderService identityProviderService;
-
-    @Autowired
-    private PasswordValidator passwordValidator;
-
-    @GET
-    @Produces(MediaType.APPLICATION_JSON)
-    @ApiOperation(value = "List users for a security domain")
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "List users for a security domain", response = User.class, responseContainer = "Set"),
-            @ApiResponse(code = 500, message = "Internal server error")})
-    public void list(@PathParam("domain") String domain,
-                                @QueryParam("q") String query,
-                                @QueryParam("page") @DefaultValue("0") int page,
-                                @QueryParam("size") @DefaultValue(MAX_USERS_SIZE_PER_PAGE_STRING) int size,
-                                @Suspended final AsyncResponse response) {
-        domainService.findById(domain)
-                .switchIfEmpty(Maybe.error(new DomainNotFoundException(domain)))
-                .flatMapSingle(irrelevant -> {
-                    if (query != null) {
-                        return userService.search(domain, query, Integer.min(size, MAX_USERS_SIZE_PER_PAGE));
-                    } else {
-                        return userService.findByDomain(domain, page, Integer.min(size, MAX_USERS_SIZE_PER_PAGE));
-                    }
-                })
-                .flatMap(pagedUsers ->
-                        Observable.fromIterable(pagedUsers.getData())
-                            .flatMapSingle(user -> {
-                                if (user.getSource() != null) {
-                                    return identityProviderService.findById(user.getSource())
-                                            .map(idP -> {
-                                                user.setSource(idP.getName());
-                                                return user;
-                                            })
-                                            .defaultIfEmpty(user)
-                                            .toSingle();
-                                }
-                                return Single.just(user);
-                            })
-                            .toSortedList(Comparator.comparing(User::getUsername))
-                            .map(users -> new Page(users, pagedUsers.getCurrentPage(), pagedUsers.getTotalCount()))
-                )
-                .map(users -> Response.ok(users).build())
-                .subscribe(
-                        result -> response.resume(result),
-                        error -> response.resume(error));
-    }
-
-    @POST
-    @Produces(MediaType.APPLICATION_JSON)
-    @Consumes(MediaType.APPLICATION_JSON)
-    @ApiOperation(value = "Create a user")
-    @ApiResponses({
-            @ApiResponse(code = 201, message = "User successfully created"),
-            @ApiResponse(code = 500, message = "Internal server error")})
-    public void create(
-            @PathParam("domain") String domain,
-            @ApiParam(name = "user", required = true)
-            @Valid @NotNull final NewUser newUser,
-            @Suspended final AsyncResponse response) {
-        final io.gravitee.am.identityprovider.api.User authenticatedUser = getAuthenticatedUser();
-
-        // user must have a password in no pre registration mode
-        if (!newUser.isPreRegistration() && newUser.getPassword() == null) {
-            response.resume(new UserInvalidException(("Field [password] is required")));
-            return;
-        }
-
-        // check password policy
-        if (newUser.getPassword() != null) {
-            if (!passwordValidator.validate(newUser.getPassword())) {
-                response.resume(new UserInvalidException(("Field [password] is invalid")));
-                return;
-            }
-        }
-
-        domainService.findById(domain)
-                .switchIfEmpty(Maybe.error(new DomainNotFoundException(domain)))
-                .flatMapSingle(userProvider -> userService.create(domain, newUser, authenticatedUser))
-                .map(user -> Response
-                        .created(URI.create("/domains/" + domain + "/users/" + user.getId()))
-                        .entity(user)
-                        .build())
-                .subscribe(
-                        result -> response.resume(result),
-                        error -> response.resume(error));
-    }
-
-    @Path("{user}")
-    public UserResource getUserResource() {
-        return resourceContext.getResource(UserResource.class);
-    }
-}
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.am.management.handlers.management.api.resources;
+
+import io.gravitee.am.management.service.UserService;
+import io.gravitee.am.model.User;
+import io.gravitee.am.model.common.Page;
+import io.gravitee.am.service.DomainService;
+import io.gravitee.am.service.IdentityProviderService;
+import io.gravitee.am.service.authentication.crypto.password.PasswordValidator;
+import io.gravitee.am.service.exception.DomainNotFoundException;
+import io.gravitee.am.service.exception.UserInvalidException;
+import io.gravitee.am.service.model.NewUser;
+import io.gravitee.common.http.MediaType;
+import io.reactivex.Maybe;
+import io.reactivex.Observable;
+import io.reactivex.Single;
+import io.swagger.annotations.*;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import javax.ws.rs.*;
+import javax.ws.rs.container.AsyncResponse;
+import javax.ws.rs.container.ResourceContext;
+import javax.ws.rs.container.Suspended;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import java.net.URI;
+import java.util.Comparator;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@Api(tags = {"user"})
+public class UsersResource extends AbstractResource {
+
+    private static final int MAX_USERS_SIZE_PER_PAGE = 30;
+    private static final String MAX_USERS_SIZE_PER_PAGE_STRING = "30";
+
+    @Context
+    private ResourceContext resourceContext;
+
+    @Autowired
+    private UserService userService;
+
+    @Autowired
+    private DomainService domainService;
+
+    @Autowired
+    private IdentityProviderService identityProviderService;
+
+    @Autowired
+    private PasswordValidator passwordValidator;
+
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(value = "List users for a security domain")
+    @ApiResponses({
+            @ApiResponse(code = 200, message = "List users for a security domain", response = User.class, responseContainer = "Set"),
+            @ApiResponse(code = 500, message = "Internal server error")})
+    public void list(@PathParam("domain") String domain,
+                                @QueryParam("q") String query,
+                                @QueryParam("page") @DefaultValue("0") int page,
+                                @QueryParam("size") @DefaultValue(MAX_USERS_SIZE_PER_PAGE_STRING) int size,
+                                @Suspended final AsyncResponse response) {
+        domainService.findById(domain)
+                .switchIfEmpty(Maybe.error(new DomainNotFoundException(domain)))
+                .flatMapSingle(irrelevant -> {
+                    if (query != null) {
+                        return userService.search(domain, query, page, Integer.min(size, MAX_USERS_SIZE_PER_PAGE));
+                    } else {
+                        return userService.findByDomain(domain, page, Integer.min(size, MAX_USERS_SIZE_PER_PAGE));
+                    }
+                })
+                .flatMap(pagedUsers ->
+                        Observable.fromIterable(pagedUsers.getData())
+                            .flatMapSingle(user -> {
+                                if (user.getSource() != null) {
+                                    return identityProviderService.findById(user.getSource())
+                                            .map(idP -> {
+                                                user.setSource(idP.getName());
+                                                return user;
+                                            })
+                                            .defaultIfEmpty(user)
+                                            .toSingle();
+                                }
+                                return Single.just(user);
+                            })
+                            .toSortedList(Comparator.comparing(User::getUsername))
+                            .map(users -> new Page(users, pagedUsers.getCurrentPage(), pagedUsers.getTotalCount()))
+                )
+                .map(users -> Response.ok(users).build())
+                .subscribe(
+                        result -> response.resume(result),
+                        error -> response.resume(error));
+    }
+
+    @POST
+    @Produces(MediaType.APPLICATION_JSON)
+    @Consumes(MediaType.APPLICATION_JSON)
+    @ApiOperation(value = "Create a user")
+    @ApiResponses({
+            @ApiResponse(code = 201, message = "User successfully created"),
+            @ApiResponse(code = 500, message = "Internal server error")})
+    public void create(
+            @PathParam("domain") String domain,
+            @ApiParam(name = "user", required = true)
+            @Valid @NotNull final NewUser newUser,
+            @Suspended final AsyncResponse response) {
+        final io.gravitee.am.identityprovider.api.User authenticatedUser = getAuthenticatedUser();
+
+        // user must have a password in no pre registration mode
+        if (!newUser.isPreRegistration() && newUser.getPassword() == null) {
+            response.resume(new UserInvalidException(("Field [password] is required")));
+            return;
+        }
+
+        // check password policy
+        if (newUser.getPassword() != null) {
+            if (!passwordValidator.validate(newUser.getPassword())) {
+                response.resume(new UserInvalidException(("Field [password] is invalid")));
+                return;
+            }
+        }
+
+        domainService.findById(domain)
+                .switchIfEmpty(Maybe.error(new DomainNotFoundException(domain)))
+                .flatMapSingle(userProvider -> userService.create(domain, newUser, authenticatedUser))
+                .map(user -> Response
+                        .created(URI.create("/domains/" + domain + "/users/" + user.getId()))
+                        .entity(user)
+                        .build())
+                .subscribe(
+                        result -> response.resume(result),
+                        error -> response.resume(error));
+    }
+
+    @Path("{user}")
+    public UserResource getUserResource() {
+        return resourceContext.getResource(UserResource.class);
+    }
+}
diff --git a/gravitee-am-management-api/gravitee-am-management-api-repository/src/main/java/io/gravitee/am/management/repository/proxy/UserRepositoryProxy.java b/gravitee-am-management-api/gravitee-am-management-api-repository/src/main/java/io/gravitee/am/management/repository/proxy/UserRepositoryProxy.java
index 4ac215228c..49bfedea63 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-repository/src/main/java/io/gravitee/am/management/repository/proxy/UserRepositoryProxy.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-repository/src/main/java/io/gravitee/am/management/repository/proxy/UserRepositoryProxy.java
@@ -44,8 +44,8 @@ public class UserRepositoryProxy extends AbstractProxy<UserRepository> implement
     }
 
     @Override
-    public Single<Page<User>> search(String domain, String query, int limit) {
-        return target.search(domain, query, limit);
+    public Single<Page<User>> search(String domain, String query, int page, int size) {
+        return target.search(domain, query, page, size);
     }
 
     @Override
diff --git a/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/UserService.java b/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/UserService.java
index 2444121ef0..de4fa868e5 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/UserService.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/UserService.java
@@ -31,7 +31,7 @@ import java.util.List;
  */
 public interface UserService {
 
-    Single<Page<User>> search(String domain, String query, int limit);
+    Single<Page<User>> search(String domain, String query, int page, int size);
 
     Single<Page<User>> findByDomain(String domain, int page, int size);
 
diff --git a/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/impl/UserServiceImpl.java b/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/impl/UserServiceImpl.java
index 12eb658a23..34e9085e44 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/impl/UserServiceImpl.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/impl/UserServiceImpl.java
@@ -100,8 +100,8 @@ public class UserServiceImpl implements UserService {
     private RoleService roleService;
 
     @Override
-    public Single<Page<User>> search(String domain, String query, int limit) {
-        return userService.search(domain, query, limit);
+    public Single<Page<User>> search(String domain, String query, int page, int size) {
+        return userService.search(domain, query, page, size);
     }
 
     @Override
diff --git a/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/management/api/UserRepository.java b/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/management/api/UserRepository.java
index 1bf36ac05e..32fc884a35 100644
--- a/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/management/api/UserRepository.java
+++ b/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/management/api/UserRepository.java
@@ -34,7 +34,7 @@ public interface UserRepository extends CrudRepository<User, String> {
 
     Single<Page<User>> findByDomain(String domain, int page, int size);
 
-    Single<Page<User>> search(String domain, String query, int limit);
+    Single<Page<User>> search(String domain, String query, int page, int size);
 
     Single<List<User>> findByDomainAndEmail(String domain, String email, boolean strict);
 
diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoUserRepository.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoUserRepository.java
index f0d4afcd17..1196f847c0 100644
--- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoUserRepository.java
+++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoUserRepository.java
@@ -84,7 +84,7 @@ public class MongoUserRepository extends AbstractManagementMongoRepository imple
     }
 
     @Override
-    public Single<Page<User>> search(String domain, String query, int limit) {
+    public Single<Page<User>> search(String domain, String query, int page, int size) {
         // currently search on username field
         Bson searchQuery = new BasicDBObject(FIELD_USERNAME, query);
         // if query contains wildcard, use the regex query
@@ -99,7 +99,7 @@ public class MongoUserRepository extends AbstractManagementMongoRepository imple
                 searchQuery);
 
         Single<Long> countOperation = Observable.fromPublisher(usersCollection.countDocuments(mongoQuery)).first(0l);
-        Single<Set<User>> usersOperation = Observable.fromPublisher(usersCollection.find(mongoQuery).limit(limit)).map(this::convert).collect(LinkedHashSet::new, Set::add);
+        Single<Set<User>> usersOperation = Observable.fromPublisher(usersCollection.find(mongoQuery).skip(size * page).limit(size)).map(this::convert).collect(LinkedHashSet::new, Set::add);
         return Single.zip(countOperation, usersOperation, (count, users) -> new Page<>(users, 0, count));
     }
 
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/UserService.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/UserService.java
index 7aa2019ed4..dab97741ed 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/UserService.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/UserService.java
@@ -37,7 +37,7 @@ public interface UserService {
 
     Single<Page<User>> findByDomain(String domain, int page, int size);
 
-    Single<Page<User>> search(String domain, String query, int limit);
+    Single<Page<User>> search(String domain, String query, int page, int size);
 
     Single<List<User>> findByIdIn(List<String> ids);
 
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/UserServiceImpl.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/UserServiceImpl.java
index 22403134d9..db692522c0 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/UserServiceImpl.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/UserServiceImpl.java
@@ -80,9 +80,9 @@ public class UserServiceImpl implements UserService {
     }
 
     @Override
-    public Single<Page<User>> search(String domain, String query, int limit) {
+    public Single<Page<User>> search(String domain, String query, int page, int size) {
         LOGGER.debug("Search users for domain {} with query {}", domain, query);
-        return userRepository.search(domain, query, limit)
+        return userRepository.search(domain, query, page, size)
                 .onErrorResumeNext(ex -> {
                     LOGGER.error("An error occurs while trying to search users for domain {} and query {}", domain, query, ex);
                     return Single.error(new TechnicalManagementException(String.format("An error occurs while trying to find users for domain %s and query %s", domain, query), ex));


Test patch:
diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/test/java/io/gravitee/am/repository/mongodb/management/MongoUserRepositoryTest.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/test/java/io/gravitee/am/repository/mongodb/management/MongoUserRepositoryTest.java
index eb2b6695d3..2a5a5ffa38 100644
--- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/test/java/io/gravitee/am/repository/mongodb/management/MongoUserRepositoryTest.java
+++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/test/java/io/gravitee/am/repository/mongodb/management/MongoUserRepositoryTest.java
@@ -24,6 +24,7 @@ import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.Set;
 
 /**
@@ -148,7 +149,7 @@ public class MongoUserRepositoryTest extends AbstractManagementRepositoryTest {
         userRepository.create(user2).blockingGet();
 
         // fetch user
-        TestObserver<Page<User>> testObserver = userRepository.search(domain, "testUsername", 10).test();
+        TestObserver<Page<User>> testObserver = userRepository.search(domain, "testUsername", 0, 10).test();
         testObserver.awaitTerminalEvent();
 
         testObserver.assertComplete();
@@ -173,12 +174,53 @@ public class MongoUserRepositoryTest extends AbstractManagementRepositoryTest {
         userRepository.create(user2).blockingGet();
 
         // fetch user
-        TestObserver<Page<User>> testObserver = userRepository.search(domain, "testUsername*", 10).test();
+        TestObserver<Page<User>> testObserver = userRepository.search(domain, "testUsername*", 0, 10).test();
         testObserver.awaitTerminalEvent();
 
         testObserver.assertComplete();
         testObserver.assertNoErrors();
         testObserver.assertValue(users -> users.getData().size() == 2);
     }
+    
+    @Test
+    public void testSearch_paged() {
+        final String domain = "domain";
+        // create user
+        User user1 = new User();
+        user1.setDomain(domain);
+        user1.setUsername("testUsername1");
+        userRepository.create(user1).blockingGet();
+
+        User user2 = new User();
+        user2.setDomain(domain);
+        user2.setUsername("testUsername2");
+        userRepository.create(user2).blockingGet();
+        
+        User user3 = new User();
+        user3.setDomain(domain);
+        user3.setUsername("testUsername3");
+        userRepository.create(user3).blockingGet();
+
+        // fetch user (page 0)
+        TestObserver<Page<User>> testObserverP0 = userRepository.search(domain, "testUsername*", 0, 2).test();
+        testObserverP0.awaitTerminalEvent();
+        
+        testObserverP0.assertComplete();
+        testObserverP0.assertNoErrors();
+        testObserverP0.assertValue(users -> users.getData().size() == 2);
+        testObserverP0.assertValue(users -> {
+        	Iterator<User> it = users.getData().iterator();
+        	return it.next().getUsername().equals(user1.getUsername()) && it.next().getUsername().equals(user2.getUsername());
+        });
+        
+        // fetch user (page 1)
+        TestObserver<Page<User>> testObserverP1 = userRepository.search(domain, "testUsername*", 1, 2).test();
+        testObserverP1.awaitTerminalEvent();
+        
+        testObserverP1.assertComplete();
+        testObserverP1.assertNoErrors();
+        testObserverP1.assertValue(users -> users.getData().size() == 1);
+        testObserverP1.assertValue(users -> users.getData().iterator().next().getUsername().equals(user3.getUsername()));
+    }
 
 }