summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>2015-04-29 16:51:26 +0200
committerJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>2015-05-06 10:57:06 +0200
commitd6338c209555f40ef41a7695427252f02edbdda4 (patch)
tree293e925876abe41f43f1b4f447abb5120cbf9cb0
parent92860747be8cbf03f57c42ca3774f0ff732acf98 (diff)
downloadsonarqube-d6338c209555f40ef41a7695427252f02edbdda4.tar.gz
sonarqube-d6338c209555f40ef41a7695427252f02edbdda4.zip
SONAR-6465 Create new Java WS to search users
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java1
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndex.java41
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexDefinition.java43
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/user/ws/SearchAction.java109
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexTest.java13
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/user/ws/SearchActionTest.java130
-rw-r--r--server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/empty.json6
-rw-r--r--server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/five_users.json47
-rw-r--r--server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_one.json47
-rw-r--r--server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_two.json47
-rw-r--r--server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/user_one.json15
11 files changed, 495 insertions, 4 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java
index 67679ebd960..9012a2c3f9a 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java
@@ -758,6 +758,7 @@ class ServerComponents {
pico.addSingleton(org.sonar.server.user.ws.CreateAction.class);
pico.addSingleton(org.sonar.server.user.ws.UpdateAction.class);
pico.addSingleton(org.sonar.server.user.ws.CurrentUserAction.class);
+ pico.addSingleton(org.sonar.server.user.ws.SearchAction.class);
pico.addSingleton(org.sonar.server.issue.ws.AuthorsAction.class);
pico.addSingleton(FavoritesWs.class);
pico.addSingleton(UserPropertiesWs.class);
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndex.java b/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndex.java
index 0cd4e4fb259..1ea3a2ff7f4 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndex.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndex.java
@@ -30,6 +30,8 @@ import org.elasticsearch.action.search.SearchType;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.BoolFilterBuilder;
import org.elasticsearch.index.query.FilterBuilders;
+import org.elasticsearch.index.query.MatchQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.sort.SortBuilders;
@@ -38,9 +40,12 @@ import org.sonar.api.ServerComponent;
import org.sonar.core.util.NonNullInputFunction;
import org.sonar.server.es.EsClient;
import org.sonar.server.es.EsUtils;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.es.SearchResult;
import org.sonar.server.exceptions.NotFoundException;
import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Iterator;
@@ -49,6 +54,10 @@ import java.util.Map;
public class UserIndex implements ServerComponent {
+ /**
+ * Convert an Elasticsearch result (a map) to an {@link UserDoc}. It's
+ * used for {@link org.sonar.server.es.SearchResult}.
+ */
private static final Function<Map<String, Object>, UserDoc> DOC_CONVERTER = new NonNullInputFunction<Map<String, Object>, UserDoc>() {
@Override
protected UserDoc doApply(Map<String, Object> input) {
@@ -69,7 +78,7 @@ public class UserIndex implements ServerComponent {
.setRouting(login);
GetResponse response = request.get();
if (response.isExists()) {
- return new UserDoc(response.getSource());
+ return DOC_CONVERTER.apply(response.getSource());
}
return null;
}
@@ -112,7 +121,7 @@ public class UserIndex implements ServerComponent {
.should(FilterBuilders.termFilter(UserIndexDefinition.FIELD_SCM_ACCOUNTS, scmAccount))))
.setSize(3);
for (SearchHit hit : request.get().getHits().getHits()) {
- result.add(new UserDoc(hit.sourceAsMap()));
+ result.add(DOC_CONVERTER.apply(hit.sourceAsMap()));
}
}
return result;
@@ -137,4 +146,32 @@ public class UserIndex implements ServerComponent {
return EsUtils.scroll(esClient, response.getScrollId(), DOC_CONVERTER);
}
+
+ public SearchResult<UserDoc> search(@Nullable String searchText, SearchOptions options) {
+ SearchRequestBuilder request = esClient.prepareSearch(UserIndexDefinition.INDEX)
+ .setTypes(UserIndexDefinition.TYPE_USER)
+ .setSize(options.getLimit())
+ .setFrom(options.getOffset())
+ .addSort(UserIndexDefinition.FIELD_NAME, SortOrder.ASC);
+
+ BoolFilterBuilder userFilter = FilterBuilders.boolFilter()
+ .must(FilterBuilders.termFilter(UserIndexDefinition.FIELD_ACTIVE, true));
+
+ QueryBuilder query = null;
+ if (StringUtils.isEmpty(searchText)) {
+ query = QueryBuilders.matchAllQuery();
+ } else {
+ query = QueryBuilders.multiMatchQuery(searchText,
+ UserIndexDefinition.FIELD_LOGIN,
+ UserIndexDefinition.FIELD_LOGIN + "." + UserIndexDefinition.SEARCH_SUB_SUFFIX,
+ UserIndexDefinition.FIELD_NAME,
+ UserIndexDefinition.FIELD_NAME + "." + UserIndexDefinition.SEARCH_SUB_SUFFIX)
+ .operator(MatchQueryBuilder.Operator.AND);
+ }
+
+ request.setQuery(QueryBuilders.filteredQuery(query,
+ userFilter));
+
+ return new SearchResult<>(request.get(), DOC_CONVERTER);
+ }
}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexDefinition.java b/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexDefinition.java
index bb07d1fc6d7..923502cf2a3 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexDefinition.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexDefinition.java
@@ -20,9 +20,13 @@
package org.sonar.server.user.index;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSortedMap;
import org.sonar.api.config.Settings;
import org.sonar.server.es.IndexDefinition;
import org.sonar.server.es.NewIndex;
+import org.sonar.server.es.NewIndex.NewIndexType;
+
+import java.util.Map;
/**
* Definition of ES index "users", including settings and fields.
@@ -41,6 +45,8 @@ public class UserIndexDefinition implements IndexDefinition {
public static final String FIELD_ACTIVE = "active";
public static final String FIELD_SCM_ACCOUNTS = "scmAccounts";
+ public static final String SEARCH_SUB_SUFFIX = "ngrams";
+
private final Settings settings;
public UserIndexDefinition(Settings settings) {
@@ -53,15 +59,48 @@ public class UserIndexDefinition implements IndexDefinition {
index.setShards(settings);
+ index.getSettings()
+ // NGram filter (not edge) for logins and names
+ .put("index.analysis.filter.ngram_filter.type", "nGram")
+ .put("index.analysis.filter.ngram_filter.min_gram", 2)
+ .put("index.analysis.filter.ngram_filter.max_gram", 15)
+ .putArray("index.analysis.filter.ngram_filter.token_chars", "letter", "digit", "punctuation", "symbol")
+
+ // NGram index analyzer
+ .put("index.analysis.analyzer.index_ngrams.type", "custom")
+ .put("index.analysis.analyzer.index_ngrams.tokenizer", "whitespace")
+ .putArray("index.analysis.analyzer.index_ngrams.filter", "trim", "lowercase", "ngram_filter")
+
+ // NGram search analyzer
+ .put("index.analysis.analyzer.search_ngrams.type", "custom")
+ .put("index.analysis.analyzer.search_ngrams.tokenizer", "whitespace")
+ .putArray("index.analysis.analyzer.search_ngrams.filter", "trim", "lowercase");
+
// type "user"
NewIndex.NewIndexType mapping = index.createType(TYPE_USER);
mapping.setAttribute("_id", ImmutableMap.of("path", FIELD_LOGIN));
- mapping.stringFieldBuilder(FIELD_LOGIN).build();
- mapping.stringFieldBuilder(FIELD_NAME).build();
+
+ mapping.stringFieldBuilder(FIELD_LOGIN).enableGramSearch().build();
+ addSubSearchField(mapping, FIELD_LOGIN);
+ mapping.stringFieldBuilder(FIELD_NAME).enableGramSearch().build();
+ addSubSearchField(mapping, FIELD_NAME);
mapping.stringFieldBuilder(FIELD_EMAIL).enableSorting().build();
mapping.createDateTimeField(FIELD_CREATED_AT);
mapping.createDateTimeField(FIELD_UPDATED_AT);
mapping.createBooleanField(FIELD_ACTIVE);
mapping.stringFieldBuilder(FIELD_SCM_ACCOUNTS).build();
}
+
+ private void addSubSearchField(NewIndexType mapping, String field) {
+ Map<String, Object> hash = (Map<String, Object>) mapping.getProperty(field);
+ if (hash == null) {
+ throw new IllegalStateException(String.format("Field %s is not defined", field));
+ }
+ Map<String, Object> multiField = (Map<String, Object>) hash.get("fields");
+ multiField.put(SEARCH_SUB_SUFFIX, ImmutableSortedMap.of(
+ "type", "string",
+ "index", "analyzed",
+ "index_analyzer", "index_ngrams",
+ "search_analyzer", "search_ngrams"));
+ }
}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/ws/SearchAction.java b/server/sonar-server/src/main/java/org/sonar/server/user/ws/SearchAction.java
new file mode 100644
index 00000000000..7030d7363ba
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/user/ws/SearchAction.java
@@ -0,0 +1,109 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.server.user.ws;
+
+import com.google.common.collect.ImmutableSet;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.es.SearchResult;
+import org.sonar.server.user.index.UserDoc;
+import org.sonar.server.user.index.UserIndex;
+
+import javax.annotation.Nullable;
+
+import java.util.List;
+import java.util.Set;
+
+public class SearchAction implements BaseUsersWsAction {
+
+ private static final String PARAM_QUERY = "q";
+
+ private static final String FIELD_LOGIN = "login";
+ private static final String FIELD_NAME = "name";
+ private static final String FIELD_EMAIL = "email";
+ private static final String FIELD_SCM_ACCOUNTS = "scmAccounts";
+ private static final Set<String> FIELDS = ImmutableSet.of(FIELD_LOGIN, FIELD_NAME, FIELD_EMAIL, FIELD_SCM_ACCOUNTS);
+
+ private final UserIndex userIndex;
+
+ public SearchAction(UserIndex userIndex) {
+ this.userIndex = userIndex;
+ }
+
+ @Override
+ public void define(WebService.NewController controller) {
+ WebService.NewAction action = controller.createAction("search2")
+ .setDescription("Get a list of active users.")
+ .setSince("3.6")
+ .setHandler(this);
+
+ action.addFieldsParam(FIELDS);
+ action.addPagingParams(50);
+
+ action.createParam(PARAM_QUERY)
+ .setDescription("Filter on login or name.");
+ }
+
+ @Override
+ public void handle(Request request, Response response) throws Exception {
+ SearchOptions options = new SearchOptions()
+ .setPage(request.mandatoryParamAsInt(WebService.Param.PAGE), request.mandatoryParamAsInt(WebService.Param.PAGE_SIZE));
+ List<String> fields = request.paramAsStrings(WebService.Param.FIELDS);
+ SearchResult<UserDoc> result = userIndex.search(request.param(PARAM_QUERY), options);
+
+ JsonWriter json = response.newJsonWriter().beginObject();
+ options.writeJson(json, result.getTotal());
+ writeUsers(json, result, fields);
+ json.endObject().close();
+ }
+
+ private void writeUsers(JsonWriter json, SearchResult<UserDoc> result, @Nullable List<String> fields) {
+
+ json.name("users").beginArray();
+ for (UserDoc user : result.getDocs()) {
+ json.beginObject();
+ writeIfNeeded(json, user.login(), FIELD_LOGIN, fields);
+ writeIfNeeded(json, user.name(), FIELD_NAME, fields);
+ writeIfNeeded(json, user.email(), FIELD_EMAIL, fields);
+ if (fieldIsWanted(FIELD_SCM_ACCOUNTS, fields)) {
+ json.name(FIELD_SCM_ACCOUNTS)
+ .beginArray()
+ .values(user.scmAccounts())
+ .endArray();
+ }
+ json.endObject();
+ }
+ json.endArray();
+ }
+
+ private void writeIfNeeded(JsonWriter json, String value, String field, @Nullable List<String> fields) {
+ if (fieldIsWanted(field, fields)) {
+ json.prop(field, value);
+ }
+ }
+
+ private boolean fieldIsWanted(String field, @Nullable List<String> fields) {
+ return fields == null || fields.isEmpty() || fields.contains(field);
+ }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexTest.java
index 5341af1911a..152393cd7f9 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexTest.java
@@ -25,6 +25,7 @@ import org.junit.ClassRule;
import org.junit.Test;
import org.sonar.api.config.Settings;
import org.sonar.server.es.EsTester;
+import org.sonar.server.es.SearchOptions;
import org.sonar.server.exceptions.NotFoundException;
import static org.assertj.core.api.Assertions.assertThat;
@@ -143,4 +144,16 @@ public class UserIndexTest {
// restrict results to 3 users
assertThat(index.getAtMostThreeActiveUsersForScmAccount("user1@mail.com")).hasSize(3);
}
+
+ @Test
+ public void searchUsers() throws Exception {
+ esTester.putDocuments(UserIndexDefinition.INDEX, UserIndexDefinition.TYPE_USER, this.getClass(),
+ "user1.json", "user2.json");
+
+ assertThat(index.search(null, new SearchOptions()).getDocs()).hasSize(2);
+ assertThat(index.search("user", new SearchOptions()).getDocs()).hasSize(2);
+ assertThat(index.search("ser", new SearchOptions()).getDocs()).hasSize(2);
+ assertThat(index.search("user1", new SearchOptions()).getDocs()).hasSize(1);
+ assertThat(index.search("user2", new SearchOptions()).getDocs()).hasSize(1);
+ }
}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/ws/SearchActionTest.java
new file mode 100644
index 00000000000..fa7dfcac236
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/user/ws/SearchActionTest.java
@@ -0,0 +1,130 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.server.user.ws;
+
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonar.api.config.Settings;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.user.index.UserDoc;
+import org.sonar.server.user.index.UserIndex;
+import org.sonar.server.user.index.UserIndexDefinition;
+import org.sonar.server.ws.WsTester;
+
+import java.util.Arrays;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SearchActionTest {
+
+ @ClassRule
+ public static final EsTester esTester = new EsTester().addDefinitions(new UserIndexDefinition(new Settings()));
+
+ WebService.Controller controller;
+
+ WsTester tester;
+
+ UserIndex index;
+
+ @Before
+ public void setUp() throws Exception {
+ esTester.truncateIndices();
+
+ index = new UserIndex(esTester.client());
+ tester = new WsTester(new UsersWs(new SearchAction(index)));
+ controller = tester.controller("api/users");
+
+ }
+
+ @Test
+ public void search_empty() throws Exception {
+ tester.newGetRequest("api/users", "search2").execute().assertJson(getClass(), "empty.json");
+ }
+
+ @Test
+ public void search_without_parameters() throws Exception {
+ injectUsers(5);
+
+ tester.newGetRequest("api/users", "search2").execute().assertJson(getClass(), "five_users.json");
+ }
+
+ @Test
+ public void search_with_query() throws Exception {
+ injectUsers(5);
+
+ tester.newGetRequest("api/users", "search2").setParam("q", "user-1").execute().assertJson(getClass(), "user_one.json");
+ }
+
+ @Test
+ public void search_with_paging() throws Exception {
+ injectUsers(10);
+
+ tester.newGetRequest("api/users", "search2").setParam("ps", "5").execute().assertJson(getClass(), "page_one.json");
+ tester.newGetRequest("api/users", "search2").setParam("ps", "5").setParam("p", "2").execute().assertJson(getClass(), "page_two.json");
+ }
+
+ @Test
+ public void search_with_fields() throws Exception {
+ injectUsers(1);
+
+ assertThat(tester.newGetRequest("api/users", "search2").execute().outputAsString())
+ .contains("login")
+ .contains("name")
+ .contains("email")
+ .contains("scmAccounts");
+
+ assertThat(tester.newGetRequest("api/users", "search2").setParam("f", "").execute().outputAsString())
+ .contains("login")
+ .contains("name")
+ .contains("email")
+ .contains("scmAccounts");
+
+ assertThat(tester.newGetRequest("api/users", "search2").setParam("f", "login").execute().outputAsString())
+ .contains("login")
+ .doesNotContain("name")
+ .doesNotContain("email")
+ .doesNotContain("scmAccounts");
+
+ assertThat(tester.newGetRequest("api/users", "search2").setParam("f", "scmAccounts").execute().outputAsString())
+ .doesNotContain("login")
+ .doesNotContain("name")
+ .doesNotContain("email")
+ .contains("scmAccounts");
+ }
+
+ private void injectUsers(int numberOfUsers) throws Exception {
+ long createdAt = System.currentTimeMillis();
+ UserDoc[] users = new UserDoc[numberOfUsers];
+ for (int index = 0; index < numberOfUsers; index++) {
+ users[index] = new UserDoc()
+ .setActive(true)
+ .setCreatedAt(createdAt)
+ .setEmail(String.format("user-%d@mail.com", index))
+ .setLogin(String.format("user-%d", index))
+ .setName(String.format("User %d", index))
+ .setScmAccounts(Arrays.asList(String.format("user-%d", index)))
+ .setUpdatedAt(createdAt);
+ }
+ esTester.putDocuments(UserIndexDefinition.INDEX, UserIndexDefinition.TYPE_USER, users);
+ }
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/empty.json b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/empty.json
new file mode 100644
index 00000000000..bce6d1f86c2
--- /dev/null
+++ b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/empty.json
@@ -0,0 +1,6 @@
+{
+ "p": 1,
+ "ps": 50,
+ "total": 0,
+ "users": []
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/five_users.json b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/five_users.json
new file mode 100644
index 00000000000..88a6fec9ecb
--- /dev/null
+++ b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/five_users.json
@@ -0,0 +1,47 @@
+{
+ "p": 1,
+ "ps": 50,
+ "total": 5,
+ "users": [
+ {
+ "login": "user-0",
+ "name": "User 0",
+ "email": "user-0@mail.com",
+ "scmAccounts": [
+ "user-0"
+ ]
+ },
+ {
+ "login": "user-1",
+ "name": "User 1",
+ "email": "user-1@mail.com",
+ "scmAccounts": [
+ "user-1"
+ ]
+ },
+ {
+ "login": "user-2",
+ "name": "User 2",
+ "email": "user-2@mail.com",
+ "scmAccounts": [
+ "user-2"
+ ]
+ },
+ {
+ "login": "user-3",
+ "name": "User 3",
+ "email": "user-3@mail.com",
+ "scmAccounts": [
+ "user-3"
+ ]
+ },
+ {
+ "login": "user-4",
+ "name": "User 4",
+ "email": "user-4@mail.com",
+ "scmAccounts": [
+ "user-4"
+ ]
+ }
+ ]
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_one.json b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_one.json
new file mode 100644
index 00000000000..3ea6c0bc020
--- /dev/null
+++ b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_one.json
@@ -0,0 +1,47 @@
+{
+ "p": 1,
+ "ps": 5,
+ "total": 10,
+ "users": [
+ {
+ "login": "user-0",
+ "name": "User 0",
+ "email": "user-0@mail.com",
+ "scmAccounts": [
+ "user-0"
+ ]
+ },
+ {
+ "login": "user-1",
+ "name": "User 1",
+ "email": "user-1@mail.com",
+ "scmAccounts": [
+ "user-1"
+ ]
+ },
+ {
+ "login": "user-2",
+ "name": "User 2",
+ "email": "user-2@mail.com",
+ "scmAccounts": [
+ "user-2"
+ ]
+ },
+ {
+ "login": "user-3",
+ "name": "User 3",
+ "email": "user-3@mail.com",
+ "scmAccounts": [
+ "user-3"
+ ]
+ },
+ {
+ "login": "user-4",
+ "name": "User 4",
+ "email": "user-4@mail.com",
+ "scmAccounts": [
+ "user-4"
+ ]
+ }
+ ]
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_two.json b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_two.json
new file mode 100644
index 00000000000..143a32adafe
--- /dev/null
+++ b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_two.json
@@ -0,0 +1,47 @@
+{
+ "p": 2,
+ "ps": 5,
+ "total": 10,
+ "users": [
+ {
+ "login": "user-5",
+ "name": "User 5",
+ "email": "user-5@mail.com",
+ "scmAccounts": [
+ "user-5"
+ ]
+ },
+ {
+ "login": "user-6",
+ "name": "User 6",
+ "email": "user-6@mail.com",
+ "scmAccounts": [
+ "user-6"
+ ]
+ },
+ {
+ "login": "user-7",
+ "name": "User 7",
+ "email": "user-7@mail.com",
+ "scmAccounts": [
+ "user-7"
+ ]
+ },
+ {
+ "login": "user-8",
+ "name": "User 8",
+ "email": "user-8@mail.com",
+ "scmAccounts": [
+ "user-8"
+ ]
+ },
+ {
+ "login": "user-9",
+ "name": "User 9",
+ "email": "user-9@mail.com",
+ "scmAccounts": [
+ "user-9"
+ ]
+ }
+ ]
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/user_one.json b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/user_one.json
new file mode 100644
index 00000000000..3c5b27e9552
--- /dev/null
+++ b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/user_one.json
@@ -0,0 +1,15 @@
+{
+ "p": 1,
+ "ps": 50,
+ "total": 1,
+ "users": [
+ {
+ "login": "user-1",
+ "name": "User 1",
+ "email": "user-1@mail.com",
+ "scmAccounts": [
+ "user-1"
+ ]
+ }
+ ]
+}