package org.sonar.db.organization;
+import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.sonar.db.Dao;
return Optional.ofNullable(mapper(dbSession).select(organizationUuid, userId));
}
+ public List<String> selectLoginsByOrganizationUuid(DbSession dbSession, String organizationUuid) {
+ return mapper(dbSession).selectLogins(organizationUuid);
+ }
+
public void insert(DbSession dbSession, OrganizationMemberDto organizationMemberDto) {
mapper(dbSession).insert(organizationMemberDto);
}
package org.sonar.db.organization;
+import java.util.List;
import java.util.Set;
import org.apache.ibatis.annotations.Param;
Set<String> selectOrganizationUuidsByUser(@Param("userId") int userId);
+ List<String> selectLogins(String organizationUuid);
+
void insert(OrganizationMemberDto organizationMember);
void delete(@Param("organizationUuid") String organizationUuid, @Param("userId") Integer userId);
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
+import com.google.common.collect.Multiset;
import java.util.Collection;
import java.util.List;
import java.util.Map;
return mapper(dbSession).selectGroupIdsByUserId(userId);
}
+ public Multiset<String> countGroupByLoginsAndOrganization(DbSession dbSession, Collection<String> logins, String organizationUuid) {
+ Multimap<String, String> result = ArrayListMultimap.create();
+ executeLargeInputs(
+ logins,
+ input -> {
+ List<LoginGroup> groupMemberships = mapper(dbSession).selectGroupsByLoginsAndOrganization(input, organizationUuid);
+ for (LoginGroup membership : groupMemberships) {
+ result.put(membership.login(), membership.groupName());
+ }
+ return groupMemberships;
+ });
+
+ return result.keys();
+ }
+
public Multimap<String, String> selectGroupsByLogins(DbSession session, Collection<String> logins) {
Multimap<String, String> result = ArrayListMultimap.create();
executeLargeInputs(
List<LoginGroup> selectGroupsByLogins(@Param("logins") List<String> logins);
+ List<LoginGroup> selectGroupsByLoginsAndOrganization(@Param("logins") List<String> logins, @Param("organizationUuid") String organizationUuid);
+
List<Integer> selectGroupIdsByUserId(@Param("userId") int userId);
-
}
and om.user_id = #{userId, jdbcType=INTEGER}
</select>
+ <select id="selectLogins" resultType="string">
+ select u.login
+ from organization_members om
+ inner join users u on om.user_id = u.id
+ where om.organization_uuid=#{organizationUuid,jdbcType=VARCHAR}
+ </select>
+
<select id="selectOrganizationUuidsByUser" resultType="String">
select om.organization_uuid as "organizationUuid"
from organization_members om
)
</insert>
- <delete id="delete" parameterType="map">
+ <delete id="delete">
delete from organization_members
where
organization_uuid = #{organizationUuid, jdbcType=VARCHAR}
ORDER BY u.login, g.name, g.id
</select>
+ <select id="selectGroupsByLoginsAndOrganization" parameterType="map" resultType="org.sonar.db.user.LoginGroup">
+ SELECT u.login as login, g.name as groupName
+ FROM users u
+ LEFT JOIN groups_users gu ON gu.user_id=u.id
+ INNER JOIN groups g ON gu.group_id=g.id
+ <where>
+ u.login in
+ <foreach collection="logins" open="(" close=")" item="login" separator=",">
+ #{login}
+ </foreach>
+ and g.organization_uuid=#{organizationUuid,jdbcType=VARCHAR}
+ </where>
+ ORDER BY u.login, g.name, g.id
+ </select>
+
<sql id="userCommonClauses">
FROM users u
LEFT JOIN groups_users gu ON gu.user_id=u.id AND gu.group_id=#{groupId}
package org.sonar.db.organization;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.ibatis.exceptions.PersistenceException;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
+import org.sonar.db.user.UserDto;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
assertThat(underTest.select(dbSession, "O2", 512)).isNotPresent();
}
+ @Test
+ public void select_logins() {
+ OrganizationDto organization = db.organizations().insert();
+ OrganizationDto anotherOrganization = db.organizations().insert();
+ UserDto user = db.users().insertUser();
+ UserDto anotherUser = db.users().insertUser();
+ UserDto userInAnotherOrganization = db.users().insertUser();
+ db.organizations().addMember(organization, user);
+ db.organizations().addMember(organization, anotherUser);
+ db.organizations().addMember(anotherOrganization, userInAnotherOrganization);
+
+ List<String> result = underTest.selectLoginsByOrganizationUuid(dbSession, organization.getUuid());
+
+ assertThat(result).containsOnly(user.getLogin(), anotherUser.getLogin());
+ }
+
@Test
public void select_organization_uuids_by_user_id() {
OrganizationDto organizationDto1 = db.organizations().insert();
}
@Test
- public void count_groups_by_login() {
+ public void count_groups_by_logins() {
dbTester.prepareDbUnit(getClass(), "shared.xml");
assertThat(underTest.selectGroupsByLogins(dbTester.getSession(), Arrays.<String>asList()).keys()).isEmpty();
package org.sonar.server.organization.ws;
+import com.google.common.collect.Multiset;
+import com.google.common.collect.Ordering;
+import com.google.common.hash.Hashing;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import javax.annotation.Nullable;
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.server.ws.WebService.Param;
+import org.sonar.api.server.ws.WebService.SelectionMode;
+import org.sonar.core.util.stream.Collectors;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.permission.OrganizationPermission;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.es.SearchResult;
+import org.sonar.server.organization.DefaultOrganizationProvider;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.user.index.UserDoc;
+import org.sonar.server.user.index.UserIndex;
+import org.sonar.server.user.index.UserQuery;
+import org.sonarqube.ws.Common;
+import org.sonarqube.ws.Organizations.SearchMembersWsResponse;
+import org.sonarqube.ws.Organizations.User;
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.sonar.core.util.Protobuf.setNullable;
import static org.sonar.server.es.SearchOptions.MAX_LIMIT;
+import static org.sonar.server.ws.WsUtils.checkFoundWithOptional;
+import static org.sonar.server.ws.WsUtils.writeProtobuf;
public class SearchMembersAction implements OrganizationsWsAction {
+
+ private final DbClient dbClient;
+ private final UserIndex userIndex;
+ private final DefaultOrganizationProvider organizationProvider;
+ private final UserSession userSession;
+
+ public SearchMembersAction(DbClient dbClient, UserIndex userIndex, DefaultOrganizationProvider organizationProvider, UserSession userSession) {
+ this.dbClient = dbClient;
+ this.userIndex = userIndex;
+ this.organizationProvider = organizationProvider;
+ this.userSession = userSession;
+ }
+
@Override
public void define(WebService.NewController context) {
WebService.NewAction action = context.createAction("search_members")
+ .setDescription("Search members of an organization")
.setResponseExample(getClass().getResource("search_members-example.json"))
.setSince("6.4")
.setInternal(true)
.setHandler(this);
- action.addSelectionModeParam();
- action.addSearchQuery("freddy", "names", "logins");
+ action.addSearchQuery("orwe", "names", "logins");
action.addPagingParams(50, MAX_LIMIT);
+ action.createParam(Param.SELECTED)
+ .setDescription("Depending on the value, show only selected items (selected=selected) or deselected items (selected=deselected).")
+ .setInternal(true)
+ .setDefaultValue(SelectionMode.SELECTED.value())
+ .setPossibleValues(SelectionMode.SELECTED.value(), SelectionMode.DESELECTED.value());
+
action.createParam("organization")
.setDescription("Organization key")
.setInternal(true)
@Override
public void handle(Request request, Response response) throws Exception {
- // TODO
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ OrganizationDto organization = getOrganization(dbSession, request.param("organization"));
+ List<String> memberLogins = dbClient.organizationMemberDao().selectLoginsByOrganizationUuid(dbSession, organization.getUuid());
+
+ UserQuery.Builder userQuery = buildUserQuery(request, memberLogins);
+ SearchOptions searchOptions = buildSearchOptions(request);
+
+ SearchResult<UserDoc> searchResults = userIndex.search(userQuery.build(), searchOptions);
+ List<String> orderedLogins = searchResults.getDocs().stream().map(UserDoc::login).collect(Collectors.toList());
+
+ List<UserDto> users = dbClient.userDao().selectByLogins(dbSession, orderedLogins).stream()
+ .sorted(Ordering.explicit(orderedLogins).onResultOf(UserDto::getLogin))
+ .collect(Collectors.toList());
+
+ Multiset<String> groupCountByLogin = null;
+ if (userSession.hasPermission(OrganizationPermission.ADMINISTER, organization)) {
+ groupCountByLogin = dbClient.groupMembershipDao().countGroupByLoginsAndOrganization(dbSession, orderedLogins, organization.getUuid());
+ }
+
+ Common.Paging wsPaging = buildWsPaging(request, searchResults);
+ SearchMembersWsResponse wsResponse = buildResponse(users, wsPaging, groupCountByLogin);
+
+ writeProtobuf(wsResponse, request, response);
+ }
+ }
+
+ private static SearchMembersWsResponse buildResponse(List<UserDto> users, Common.Paging wsPaging, @Nullable Multiset<String> groupCountByLogin) {
+ SearchMembersWsResponse.Builder response = SearchMembersWsResponse.newBuilder();
+
+ User.Builder wsUser = User.newBuilder();
+ users.stream()
+ .map(userDto -> {
+ String login = userDto.getLogin();
+ wsUser
+ .clear()
+ .setLogin(login)
+ .setName(userDto.getName());
+ setNullable(userDto.getEmail(), text -> wsUser.setAvatar(hash(text)));
+ setNullable(groupCountByLogin, count -> wsUser.setGroupCount(groupCountByLogin.count(login)));
+ return wsUser;
+ })
+ .forEach(response::addUsers);
+ response.setPaging(wsPaging);
+
+ return response.build();
+ }
+
+ private static UserQuery.Builder buildUserQuery(Request request, List<String> memberLogins) {
+ UserQuery.Builder userQuery = UserQuery.builder();
+ String textQuery = request.param(Param.TEXT_QUERY);
+ checkArgument(textQuery == null || textQuery.length() >= 2, "Query length must be greater than or equal to 2");
+ userQuery.setTextQuery(textQuery);
+
+ SelectionMode selectionMode = SelectionMode.fromParam(request.mandatoryParam(Param.SELECTED));
+ if (SelectionMode.DESELECTED.equals(selectionMode)) {
+ userQuery.setExcludedLogins(memberLogins);
+ } else {
+ userQuery.setLogins(memberLogins);
+ }
+ return userQuery;
+ }
+
+ private static SearchOptions buildSearchOptions(Request request) {
+ int pageSize = request.mandatoryParamAsInt(Param.PAGE_SIZE);
+ checkArgument(pageSize <= SearchOptions.MAX_LIMIT, "Page size must lower than or equal to %s", SearchOptions.MAX_LIMIT);
+
+ return new SearchOptions().setPage(request.mandatoryParamAsInt(Param.PAGE), pageSize);
+ }
+
+ private static String hash(String text) {
+ return Hashing.md5().hashString(text.toLowerCase(Locale.ENGLISH), StandardCharsets.UTF_8).toString();
+ }
+
+ private static Common.Paging buildWsPaging(Request request, SearchResult<UserDoc> searchResults) {
+ return Common.Paging.newBuilder()
+ .setPageIndex(request.mandatoryParamAsInt(Param.PAGE))
+ .setPageSize(request.mandatoryParamAsInt(Param.PAGE_SIZE))
+ .setTotal((int) searchResults.getTotal())
+ .build();
+ }
+
+ private OrganizationDto getOrganization(DbSession dbSession, @Nullable String organizationParam) {
+ String organizationKey = Optional.ofNullable(organizationParam)
+ .orElseGet(organizationProvider.get()::getKey);
+ return checkFoundWithOptional(
+ dbClient.organizationDao().selectByKey(dbSession, organizationKey),
+ "No organization with key '%s'", organizationKey);
}
}
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
import org.apache.commons.lang.StringUtils;
import org.elasticsearch.action.get.GetRequestBuilder;
import org.elasticsearch.action.get.GetResponse;
return EsUtils.scroll(esClient, response.getScrollId(), DOC_CONVERTER);
}
- public SearchResult<UserDoc> search(@Nullable String searchText, SearchOptions options) {
+ public SearchResult<UserDoc> search(UserQuery userQuery, SearchOptions options) {
SearchRequestBuilder request = esClient.prepareSearch(UserIndexDefinition.INDEX_TYPE_USER)
.setSize(options.getLimit())
.setFrom(options.getOffset())
.addSort(FIELD_NAME, SortOrder.ASC);
- BoolQueryBuilder filter = boolQuery()
- .must(termQuery(FIELD_ACTIVE, true));
+ BoolQueryBuilder filter = boolQuery().must(termQuery(FIELD_ACTIVE, true));
+
+ userQuery.getLogins().ifPresent(
+ logins -> filter.must(termsQuery(FIELD_LOGIN, userQuery.getLogins().get())));
+
+ userQuery.getExcludedLogins().ifPresent(
+ excludedLogins -> filter.mustNot(termsQuery(FIELD_LOGIN, userQuery.getExcludedLogins().get())));
- QueryBuilder query;
- if (StringUtils.isEmpty(searchText)) {
- query = matchAllQuery();
- } else {
- query = QueryBuilders.multiMatchQuery(searchText,
+ QueryBuilder esQuery = matchAllQuery();
+ Optional<String> textQuery = userQuery.getTextQuery();
+ if (textQuery.isPresent()) {
+ esQuery = QueryBuilders.multiMatchQuery(textQuery.get(),
FIELD_LOGIN,
USER_SEARCH_GRAMS_ANALYZER.subField(FIELD_LOGIN),
FIELD_NAME,
.operator(MatchQueryBuilder.Operator.AND);
}
- request.setQuery(boolQuery().must(query).filter(filter));
+ request.setQuery(boolQuery().must(esQuery).filter(filter));
return new SearchResult<>(request.get(), DOC_CONVERTER);
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.index;
+
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import static org.apache.commons.lang.StringUtils.isBlank;
+
+@Immutable
+public class UserQuery {
+ private final String textQuery;
+ private final List<String> logins;
+ private final List<String> excludedLogins;
+
+ private UserQuery(Builder builder) {
+ this.textQuery = builder.textQuery;
+ this.logins = builder.logins == null ? null : ImmutableList.copyOf(builder.logins);
+ this.excludedLogins = builder.excludedLogins == null ? null : ImmutableList.copyOf(builder.excludedLogins);
+ }
+
+ public Optional<String> getTextQuery() {
+ return Optional.ofNullable(textQuery);
+ }
+
+ public Optional<List<String>> getLogins() {
+ return Optional.ofNullable(logins);
+ }
+
+ public Optional<List<String>> getExcludedLogins() {
+ return Optional.ofNullable(excludedLogins);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private String textQuery;
+ private List<String> logins;
+ private List<String> excludedLogins;
+
+ private Builder() {
+ // enforce factory method
+ }
+
+ public UserQuery build() {
+ return new UserQuery(this);
+ }
+
+ public Builder setTextQuery(@Nullable String textQuery) {
+ this.textQuery = isBlank(textQuery) ? null : textQuery;
+ return this;
+ }
+
+ public Builder setLogins(@Nullable List<String> logins) {
+ this.logins = logins;
+ return this;
+ }
+
+ public Builder setExcludedLogins(@Nullable List<String> excludedLogins) {
+ this.excludedLogins = excludedLogins;
+ return this;
+ }
+ }
+}
import org.sonar.server.es.SearchResult;
import org.sonar.server.user.index.UserDoc;
import org.sonar.server.user.index.UserIndex;
+import org.sonar.server.user.index.UserQuery;
import static com.google.common.base.MoreObjects.firstNonNull;
import static org.sonar.server.es.SearchOptions.MAX_LIMIT;
SearchOptions options = new SearchOptions()
.setPage(request.mandatoryParamAsInt(Param.PAGE), request.mandatoryParamAsInt(Param.PAGE_SIZE));
List<String> fields = request.paramAsStrings(Param.FIELDS);
- SearchResult<UserDoc> result = userIndex.search(request.param(Param.TEXT_QUERY), options);
+ String textQuery = request.param(Param.TEXT_QUERY);
+ SearchResult<UserDoc> result = userIndex.search(UserQuery.builder().setTextQuery(textQuery).build(), options);
try (DbSession dbSession = dbClient.openSession(false)) {
List<String> logins = Lists.transform(result.getDocs(), UserDocToLogin.INSTANCE);
{
+ "users": [
+ {
+ "login": "ada.lovelace",
+ "name": "Ada Lovelace",
+ "avatar": "680b0001b4952664631511a81a4edc59"
+ },
+ {
+ "login": "grace.hopper",
+ "name": "Grace Hopper",
+ "avatar": "36d90b0b8cc8639e4960e46116dce02c"
+ }
+ ],
+ "paging": {
+ "pageIndex": 1,
+ "pageSize": 50,
+ "total": 2
+ }
}
package org.sonar.server.organization.ws;
+import com.google.common.base.Throwables;
+import java.io.IOException;
+import java.util.stream.IntStream;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.config.MapSettings;
import org.sonar.api.server.ws.WebService;
+import org.sonar.api.server.ws.WebService.Param;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbTester;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.permission.OrganizationPermission;
+import org.sonar.db.user.GroupDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.organization.DefaultOrganizationProvider;
+import org.sonar.server.organization.TestDefaultOrganizationProvider;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.user.index.UserIndex;
+import org.sonar.server.user.index.UserIndexDefinition;
+import org.sonar.server.user.index.UserIndexer;
+import org.sonar.server.ws.TestRequest;
import org.sonar.server.ws.WsActionTester;
+import org.sonarqube.ws.Common.Paging;
+import org.sonarqube.ws.MediaTypes;
+import org.sonarqube.ws.Organizations.SearchMembersWsResponse;
+import org.sonarqube.ws.Organizations.User;
+import org.sonarqube.ws.client.organization.SearchMembersWsRequest;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.sonar.core.util.Protobuf.setNullable;
+import static org.sonar.test.JsonAssert.assertJson;
public class SearchMembersActionTest {
- private WsActionTester ws = new WsActionTester(new SearchMembersAction());
+ @Rule
+ public UserSessionRule userSession = UserSessionRule.standalone();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ @Rule
+ public EsTester es = new EsTester(new UserIndexDefinition(new MapSettings()));
+
+ @Rule
+ public DbTester db = DbTester.create();
+ private DbClient dbClient = db.getDbClient();
+
+ private DefaultOrganizationProvider organizationProvider = TestDefaultOrganizationProvider.from(db);
+ private UserIndexer indexer = new UserIndexer(dbClient, es.client());
+
+ private WsActionTester ws = new WsActionTester(new SearchMembersAction(dbClient, new UserIndex(es.client()), organizationProvider, userSession));
+
+ private SearchMembersWsRequest request = new SearchMembersWsRequest();
+
+ @Test
+ public void empty_response() {
+ SearchMembersWsResponse result = call();
+
+ assertThat(result.getUsersList()).isEmpty();
+ assertThat(result.getPaging())
+ .extracting(Paging::getPageIndex, Paging::getPageSize, Paging::getTotal)
+ .containsExactly(1, 50, 0);
+ }
+
+ @Test
+ public void search_members_of_default_organization() {
+ OrganizationDto defaultOrganization = db.getDefaultOrganization();
+ OrganizationDto anotherOrganization = db.organizations().insert();
+ UserDto user = insertUser();
+ UserDto anotherUser = insertUser();
+ UserDto userInAnotherOrganization = insertUser();
+ db.organizations().addMember(defaultOrganization, user);
+ db.organizations().addMember(defaultOrganization, anotherUser);
+ db.organizations().addMember(anotherOrganization, userInAnotherOrganization);
+
+ SearchMembersWsResponse result = call();
+
+ assertThat(result.getUsersList())
+ .extracting(User::getLogin, User::getName)
+ .containsOnly(
+ tuple(user.getLogin(), user.getName()),
+ tuple(anotherUser.getLogin(), anotherUser.getName()));
+ }
+
+ @Test
+ public void search_members_of_specified_organization() {
+ OrganizationDto organization = db.organizations().insert();
+ OrganizationDto anotherOrganization = db.organizations().insert();
+ UserDto user = insertUser();
+ UserDto anotherUser = insertUser();
+ UserDto userInAnotherOrganization = insertUser();
+ db.organizations().addMember(organization, user);
+ db.organizations().addMember(organization, anotherUser);
+ db.organizations().addMember(anotherOrganization, userInAnotherOrganization);
+ request.setOrganization(organization.getKey());
+
+ SearchMembersWsResponse result = call();
+
+ assertThat(result.getUsersList())
+ .extracting(User::getLogin, User::getName)
+ .containsOnly(
+ tuple(user.getLogin(), user.getName()),
+ tuple(anotherUser.getLogin(), anotherUser.getName()));
+ }
+
+ @Test
+ public void return_avatar() {
+ UserDto user = db.users().insertUser(u -> u.setEmail("email@domain.com"));
+ indexer.index(user.getLogin());
+ db.organizations().addMember(db.getDefaultOrganization(), user);
+
+ SearchMembersWsResponse result = call();
+
+ assertThat(result.getUsers(0).getAvatar()).isEqualTo("7328fddefd53de471baeb6e2b764f78a");
+ }
+
+ @Test
+ public void do_not_return_group_count_if_no_admin_permission() {
+ UserDto user = insertUser();
+ GroupDto group = db.users().insertGroup();
+ db.users().insertMember(group, user);
+ db.organizations().addMember(db.getDefaultOrganization(), user);
+
+ SearchMembersWsResponse result = call();
+
+ assertThat(result.getUsers(0).hasGroupCount()).isFalse();
+ }
+
+ @Test
+ public void return_group_counts_if_org_admin() {
+ userSession.addPermission(OrganizationPermission.ADMINISTER, db.getDefaultOrganization());
+ UserDto user = insertUser();
+ UserDto anotherUser = insertUser();
+ IntStream.range(0, 10)
+ .mapToObj(i -> db.users().insertGroup())
+ .forEach(g -> db.users().insertMembers(g, user));
+ OrganizationDto anotherOrganization = db.organizations().insert();
+ GroupDto anotherGroup = db.users().insertGroup(anotherOrganization);
+ db.users().insertMember(anotherGroup, user);
+ db.organizations().addMember(db.getDefaultOrganization(), user);
+ db.organizations().addMember(db.getDefaultOrganization(), anotherUser);
+ db.organizations().addMember(anotherOrganization, user);
+
+ SearchMembersWsResponse result = call();
+
+ assertThat(result.getUsersList()).extracting(User::getLogin, User::getGroupCount).containsOnly(
+ tuple(user.getLogin(), 10),
+ tuple(anotherUser.getLogin(), 0));
+ }
+
+ @Test
+ public void search_non_members() {
+ OrganizationDto defaultOrganization = db.getDefaultOrganization();
+ OrganizationDto anotherOrganization = db.organizations().insert();
+ UserDto user = insertUser();
+ UserDto anotherUser = insertUser();
+ UserDto userInAnotherOrganization = insertUser();
+ db.organizations().addMember(anotherOrganization, user);
+ db.organizations().addMember(anotherOrganization, anotherUser);
+ db.organizations().addMember(defaultOrganization, userInAnotherOrganization);
+ request.setSelected(WebService.SelectionMode.DESELECTED.value());
+
+ SearchMembersWsResponse result = call();
+
+ assertThat(result.getUsersList())
+ .extracting(User::getLogin, User::getName)
+ .containsOnly(
+ tuple(user.getLogin(), user.getName()),
+ tuple(anotherUser.getLogin(), anotherUser.getName()));
+ }
+
+ @Test
+ public void search_members_pagination() {
+ IntStream.range(0, 10).forEach(i -> {
+ UserDto userDto = db.users().insertUser(user -> user.setName("USER_" + i));
+ db.organizations().addMember(db.getDefaultOrganization(), userDto);
+ indexer.index(userDto.getLogin());
+ });
+ request.setPage(2).setPageSize(3);
+
+ SearchMembersWsResponse result = call();
+
+ assertThat(result.getUsersList()).extracting(User::getName)
+ .containsExactly("USER_3", "USER_4", "USER_5");
+ assertThat(result.getPaging())
+ .extracting(Paging::getPageIndex, Paging::getPageSize, Paging::getTotal)
+ .containsExactly(2, 3, 10);
+ }
+
+ @Test
+ public void search_members_by_name() {
+ IntStream.range(0, 10).forEach(i -> {
+ UserDto userDto = db.users().insertUser(user -> user.setName("USER_" + i));
+ db.organizations().addMember(db.getDefaultOrganization(), userDto);
+ indexer.index(userDto.getLogin());
+ });
+ request.setQuery("_9");
+
+ SearchMembersWsResponse result = call();
+
+ assertThat(result.getUsersList()).extracting(User::getName).containsExactly("USER_9");
+ }
+
+ @Test
+ public void search_members_by_login() {
+ IntStream.range(0, 10).forEach(i -> {
+ UserDto userDto = db.users().insertUser(user -> user.setLogin("USER_" + i));
+ db.organizations().addMember(db.getDefaultOrganization(), userDto);
+ indexer.index(userDto.getLogin());
+ });
+ request.setQuery("_9");
+
+ SearchMembersWsResponse result = call();
+
+ assertThat(result.getUsersList()).extracting(User::getLogin).containsExactly("USER_9");
+ }
+
+ @Test
+ public void search_members_by_email() {
+ IntStream.range(0, 10).forEach(i -> {
+ UserDto userDto = db.users().insertUser(user -> user
+ .setLogin("L" + i)
+ .setEmail("USER_" + i + "@email.com"));
+ db.organizations().addMember(db.getDefaultOrganization(), userDto);
+ indexer.index(userDto.getLogin());
+ });
+ request.setQuery("_9");
+
+ SearchMembersWsResponse result = call();
+
+ assertThat(result.getUsersList()).extracting(User::getLogin).containsExactly("L9");
+ }
+
+ @Test
+ public void json_example() {
+ UserDto ada = db.users().insertUser(u -> u.setLogin("ada.lovelace").setName("Ada Lovelace").setEmail("ada@lovelace.com"));
+ indexer.index(ada.getLogin());
+ UserDto grace = db.users().insertUser(u -> u.setLogin("grace.hopper").setName("Grace Hopper").setEmail("grace@hopper.com"));
+ indexer.index(grace.getLogin());
+ db.organizations().addMember(db.getDefaultOrganization(), ada);
+ db.organizations().addMember(db.getDefaultOrganization(), grace);
+
+ String result = ws.newRequest().execute().getInput();
+
+ assertJson(result).isSimilarTo(ws.getDef().responseExampleAsString());
+ }
+
+ @Test
+ public void fail_if_organization_is_unknown() {
+ request.setOrganization("ORGA 42");
+
+ expectedException.expect(NotFoundException.class);
+ expectedException.expectMessage("No organization with key 'ORGA 42'");
+
+ call();
+ }
+
+ @Test
+ public void fail_if_page_size_greater_than_500() {
+ request.setPageSize(501);
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Page size must lower than or equal to 500");
+
+ call();
+ }
+
+ @Test
+ public void fail_if_query_length_lower_than_2() {
+ request.setQuery("a");
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Query length must be greater than or equal to 2");
+
+ call();
+ }
@Test
public void definition() {
WebService.Action action = ws.getDef();
assertThat(action.key()).isEqualTo("search_members");
- assertThat(action.params()).extracting(WebService.Param::key)
- .containsOnly("q", "selected", "p", "ps", "organization");
+ assertThat(action.params()).extracting(Param::key).containsOnly("q", "selected", "p", "ps", "organization");
+ assertThat(action.description()).isNotEmpty();
assertThat(action.responseExampleAsString()).isNotEmpty();
assertThat(action.since()).isEqualTo("6.4");
assertThat(action.isInternal()).isTrue();
assertThat(action.isPost()).isFalse();
+ assertThat(action.param("organization").isInternal()).isTrue();
+
+ Param selected = action.param("selected");
+ assertThat(selected.possibleValues()).containsOnly("selected", "deselected");
+ assertThat(selected.isInternal()).isTrue();
+ assertThat(selected.defaultValue()).isEqualTo("selected");
+ }
+
+ private UserDto insertUser() {
+ UserDto userDto = db.users().insertUser();
+ indexer.index(userDto.getLogin());
+
+ return userDto;
+ }
+
+ private SearchMembersWsResponse call() {
+ TestRequest wsRequest = ws.newRequest()
+ .setMediaType(MediaTypes.PROTOBUF);
+ setNullable(request.getOrganization(), o -> wsRequest.setParam("organization", o));
+ setNullable(request.getQuery(), q -> wsRequest.setParam(Param.TEXT_QUERY, q));
+ setNullable(request.getPage(), p -> wsRequest.setParam(Param.PAGE, String.valueOf(p)));
+ setNullable(request.getPageSize(), ps -> wsRequest.setParam(Param.PAGE_SIZE, String.valueOf(ps)));
+ setNullable(request.getSelected(), s -> wsRequest.setParam(Param.SELECTED, s));
+
+ try {
+ return SearchMembersWsResponse.parseFrom(wsRequest.execute().getInputStream());
+ } catch (IOException e) {
+ throw Throwables.propagate(e);
+ }
}
}
*/
package org.sonar.server.user.index;
-import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import org.sonar.api.config.MapSettings;
import org.sonar.server.es.EsTester;
import org.sonar.server.es.SearchOptions;
+import org.sonar.server.es.SearchResult;
import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.sonar.server.user.index.UserIndexDefinition.INDEX_TYPE_USER;
private UserIndex underTest;
+ private UserQuery.Builder userQuery = UserQuery.builder();
+
@Before
public void setUp() {
underTest = new UserIndex(esTester.client());
@Test
public void searchUsers() throws Exception {
- esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser(USER1_LOGIN, Arrays.asList("user_1", "u1")).setEmail("email1"));
+ esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser(USER1_LOGIN, asList("user_1", "u1")).setEmail("email1"));
esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser(USER2_LOGIN, Collections.<String>emptyList()).setEmail("email2"));
- assertThat(underTest.search(null, new SearchOptions()).getDocs()).hasSize(2);
- assertThat(underTest.search("user", new SearchOptions()).getDocs()).hasSize(2);
- assertThat(underTest.search("ser", new SearchOptions()).getDocs()).hasSize(2);
- assertThat(underTest.search(USER1_LOGIN, new SearchOptions()).getDocs()).hasSize(1);
- assertThat(underTest.search(USER2_LOGIN, new SearchOptions()).getDocs()).hasSize(1);
- assertThat(underTest.search("mail", new SearchOptions()).getDocs()).hasSize(2);
- assertThat(underTest.search("EMAIL1", new SearchOptions()).getDocs()).hasSize(1);
+ assertThat(underTest.search(userQuery.build(), new SearchOptions()).getDocs()).hasSize(2);
+ assertThat(underTest.search(userQuery.setTextQuery("user").build(), new SearchOptions()).getDocs()).hasSize(2);
+ assertThat(underTest.search(userQuery.setTextQuery("ser").build(), new SearchOptions()).getDocs()).hasSize(2);
+ assertThat(underTest.search(userQuery.setTextQuery(USER1_LOGIN).build(), new SearchOptions()).getDocs()).hasSize(1);
+ assertThat(underTest.search(userQuery.setTextQuery(USER2_LOGIN).build(), new SearchOptions()).getDocs()).hasSize(1);
+ assertThat(underTest.search(userQuery.setTextQuery("mail").build(), new SearchOptions()).getDocs()).hasSize(2);
+ assertThat(underTest.search(userQuery.setTextQuery("EMAIL1").build(), new SearchOptions()).getDocs()).hasSize(1);
+ }
+
+ @Test
+ public void search_users_filter_logins() throws Exception {
+ esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser(USER1_LOGIN, asList("user_1", "u1")).setEmail("email1"));
+ esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser(USER2_LOGIN, emptyList()).setEmail("email2"));
+ esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser("user3", emptyList()).setEmail("email2"));
+
+ SearchResult<UserDoc> result = underTest.search(userQuery.setLogins(asList(USER1_LOGIN, USER2_LOGIN)).build(), new SearchOptions());
+ assertThat(result.getDocs()).hasSize(2)
+ .extracting(UserDoc::login)
+ .containsOnly(USER1_LOGIN, USER2_LOGIN);
+ }
+
+ @Test
+ public void search_users_filter_logins_that_match_exactly() {
+ esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser("USER_1", asList("user_1", "u1")).setEmail("email1"));
+ esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser("UsEr_1", emptyList()).setEmail("email2"));
+
+ assertThat(underTest.search(userQuery.setLogins(singletonList("USER_1")).build(), new SearchOptions()).getDocs()).hasSize(1);
+ assertThat(underTest.search(userQuery.setLogins(singletonList("USER_")).build(), new SearchOptions()).getDocs()).isEmpty();
+ assertThat(underTest.search(userQuery.setLogins(emptyList()).build(), new SearchOptions()).getDocs()).isEmpty();
+ }
+
+ @Test
+ public void search_users_exclude_logins() throws Exception {
+ esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser(USER1_LOGIN, asList("user_1", "u1")).setEmail("email1"));
+ esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser(USER2_LOGIN, emptyList()).setEmail("email2"));
+ esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser("user3", emptyList()).setEmail("email2"));
+
+ assertThat(underTest.search(userQuery.setExcludedLogins(emptyList()).build(), new SearchOptions()).getDocs()).hasSize(3);
+ assertThat(underTest.search(userQuery.setExcludedLogins(asList(USER1_LOGIN, USER2_LOGIN, "user3")).build(), new SearchOptions()).getDocs()).isEmpty();
+ assertThat(underTest.search(userQuery.setExcludedLogins(asList(USER1_LOGIN, USER2_LOGIN)).build(), new SearchOptions()).getDocs()).hasSize(1);
}
private static UserDoc newUser(String login, List<String> scmAccounts) {
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.sonarqube.ws.client.organization;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+public class SearchMembersWsRequest {
+ private String organization;
+ private String selected;
+ private String query;
+ private Integer page;
+ private Integer pageSize;
+
+ @CheckForNull
+ public String getOrganization() {
+ return organization;
+ }
+
+ public SearchMembersWsRequest setOrganization(@Nullable String organization) {
+ this.organization = organization;
+ return this;
+ }
+
+ @CheckForNull
+ public String getSelected() {
+ return selected;
+ }
+
+ public SearchMembersWsRequest setSelected(@Nullable String selected) {
+ this.selected = selected;
+ return this;
+ }
+
+ @CheckForNull
+ public String getQuery() {
+ return query;
+ }
+
+ public SearchMembersWsRequest setQuery(@Nullable String query) {
+ this.query = query;
+ return this;
+ }
+
+ @CheckForNull
+ public Integer getPage() {
+ return page;
+ }
+
+ public SearchMembersWsRequest setPage(@Nullable Integer page) {
+ this.page = page;
+ return this;
+ }
+
+ @CheckForNull
+ public Integer getPageSize() {
+ return pageSize;
+ }
+
+ public SearchMembersWsRequest setPageSize(@Nullable Integer pageSize) {
+ this.pageSize = pageSize;
+ return this;
+ }
+}
package sonarqube.ws.organizations;
+import "ws-commons.proto";
+
option java_package = "org.sonarqube.ws";
option java_outer_classname = "Organizations";
option optimize_for = SPEED;
repeated Organization organizations = 1;
}
+// WS api/organizations/search_members
+message SearchMembersWsResponse {
+ optional sonarqube.ws.commons.Paging paging = 1;
+ repeated User users = 2;
+}
+
message Organization {
optional string key = 1;
optional string name = 2;
optional string avatar = 5;
optional bool guarded = 6;
}
+
+message User {
+ optional string login = 1;
+ optional string name = 2;
+ optional string avatar = 3;
+ optional int32 groupCount = 4;
+}