]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6465 Create new Java WS to search users
authorJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Wed, 29 Apr 2015 14:51:26 +0000 (16:51 +0200)
committerJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Wed, 6 May 2015 08:57:06 +0000 (10:57 +0200)
server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java
server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndex.java
server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexDefinition.java
server/sonar-server/src/main/java/org/sonar/server/user/ws/SearchAction.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexTest.java
server/sonar-server/src/test/java/org/sonar/server/user/ws/SearchActionTest.java [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/empty.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/five_users.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_one.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_two.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/user_one.json [new file with mode: 0644]

index 67679ebd960f166023ae24e90c24a26bd553554e..9012a2c3f9a578c660aa3e417af8b4e480cd0f1e 100644 (file)
@@ -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);
index 0cd4e4fb259422056f7bd5dd096af9cdae324a11..1ea3a2ff7f40731fbb2a8d56969906c2ee9ef430 100644 (file)
@@ -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);
+  }
 }
index bb07d1fc6d7e283ec6822869eaf0b73b523eb000..923502cf2a3ef2f95a2be7afbc23d46d11addf51 100644 (file)
 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 (file)
index 0000000..7030d73
--- /dev/null
@@ -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);
+  }
+}
index 5341af1911aa903632e8af8d7a4fd36c192169be..152393cd7f9c82ba433da1db6cab671865462428 100644 (file)
@@ -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 (file)
index 0000000..fa7dfca
--- /dev/null
@@ -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 (file)
index 0000000..bce6d1f
--- /dev/null
@@ -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 (file)
index 0000000..88a6fec
--- /dev/null
@@ -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 (file)
index 0000000..3ea6c0b
--- /dev/null
@@ -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 (file)
index 0000000..143a32a
--- /dev/null
@@ -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 (file)
index 0000000..3c5b27e
--- /dev/null
@@ -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"
+      ]
+    }
+  ]
+}