From d6338c209555f40ef41a7695427252f02edbdda4 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lievremont Date: Wed, 29 Apr 2015 16:51:26 +0200 Subject: [PATCH] SONAR-6465 Create new Java WS to search users --- .../server/platform/ServerComponents.java | 1 + .../sonar/server/user/index/UserIndex.java | 41 +++++- .../user/index/UserIndexDefinition.java | 43 +++++- .../sonar/server/user/ws/SearchAction.java | 109 +++++++++++++++ .../server/user/index/UserIndexTest.java | 13 ++ .../server/user/ws/SearchActionTest.java | 130 ++++++++++++++++++ .../user/ws/SearchActionTest/empty.json | 6 + .../user/ws/SearchActionTest/five_users.json | 47 +++++++ .../user/ws/SearchActionTest/page_one.json | 47 +++++++ .../user/ws/SearchActionTest/page_two.json | 47 +++++++ .../user/ws/SearchActionTest/user_one.json | 15 ++ 11 files changed, 495 insertions(+), 4 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/user/ws/SearchAction.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/user/ws/SearchActionTest.java create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/empty.json create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/five_users.json create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_one.json create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_two.json create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/user_one.json 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, UserDoc> DOC_CONVERTER = new NonNullInputFunction, UserDoc>() { @Override protected UserDoc doApply(Map 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 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 hash = (Map) mapping.getProperty(field); + if (hash == null) { + throw new IllegalStateException(String.format("Field %s is not defined", field)); + } + Map multiField = (Map) 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 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 fields = request.paramAsStrings(WebService.Param.FIELDS); + SearchResult 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 result, @Nullable List 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 fields) { + if (fieldIsWanted(field, fields)) { + json.prop(field, value); + } + } + + private boolean fieldIsWanted(String field, @Nullable List 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" + ] + } + ] +} -- 2.39.5