Browse Source

SONAR-8894 Search members of an organization WS api/organizations/search_members

tags/6.4-RC1
Teryk Bellahsene 7 years ago
parent
commit
24549284b7
17 changed files with 771 additions and 29 deletions
  1. 5
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/organization/OrganizationMemberDao.java
  2. 3
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/organization/OrganizationMemberMapper.java
  3. 16
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupMembershipDao.java
  4. 2
    1
      server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupMembershipMapper.java
  5. 8
    1
      server/sonar-db-dao/src/main/resources/org/sonar/db/organization/OrganizationMemberMapper.xml
  6. 15
    0
      server/sonar-db-dao/src/main/resources/org/sonar/db/user/GroupMembershipMapper.xml
  7. 18
    0
      server/sonar-db-dao/src/test/java/org/sonar/db/organization/OrganizationMemberDaoTest.java
  8. 1
    1
      server/sonar-db-dao/src/test/java/org/sonar/db/user/GroupMembershipDaoTest.java
  9. 138
    3
      server/sonar-server/src/main/java/org/sonar/server/organization/ws/SearchMembersAction.java
  10. 14
    10
      server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndex.java
  11. 87
    0
      server/sonar-server/src/main/java/org/sonar/server/user/index/UserQuery.java
  12. 3
    1
      server/sonar-server/src/main/java/org/sonar/server/user/ws/SearchAction.java
  13. 17
    0
      server/sonar-server/src/main/resources/org/sonar/server/organization/ws/search_members-example.json
  14. 301
    3
      server/sonar-server/src/test/java/org/sonar/server/organization/ws/SearchMembersActionTest.java
  15. 46
    9
      server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexTest.java
  16. 82
    0
      sonar-ws/src/main/java/org/sonarqube/ws/client/organization/SearchMembersWsRequest.java
  17. 15
    0
      sonar-ws/src/main/protobuf/ws-organizations.proto

+ 5
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/organization/OrganizationMemberDao.java View File

@@ -20,6 +20,7 @@

package org.sonar.db.organization;

import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.sonar.db.Dao;
@@ -34,6 +35,10 @@ public class OrganizationMemberDao implements 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);
}

+ 3
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/organization/OrganizationMemberMapper.java View File

@@ -20,6 +20,7 @@

package org.sonar.db.organization;

import java.util.List;
import java.util.Set;
import org.apache.ibatis.annotations.Param;

@@ -28,6 +29,8 @@ public interface OrganizationMemberMapper {

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);

+ 16
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupMembershipDao.java View File

@@ -23,6 +23,7 @@ import com.google.common.collect.ArrayListMultimap;
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;
@@ -73,6 +74,21 @@ public class GroupMembershipDao implements Dao {
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(

+ 2
- 1
server/sonar-db-dao/src/main/java/org/sonar/db/user/GroupMembershipMapper.java View File

@@ -38,6 +38,7 @@ public interface GroupMembershipMapper {

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);
}

+ 8
- 1
server/sonar-db-dao/src/main/resources/org/sonar/db/organization/OrganizationMemberMapper.xml View File

@@ -17,6 +17,13 @@
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
@@ -36,7 +43,7 @@
)
</insert>

<delete id="delete" parameterType="map">
<delete id="delete">
delete from organization_members
where
organization_uuid = #{organizationUuid, jdbcType=VARCHAR}

+ 15
- 0
server/sonar-db-dao/src/main/resources/org/sonar/db/user/GroupMembershipMapper.xml View File

@@ -59,6 +59,21 @@
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}

+ 18
- 0
server/sonar-db-dao/src/test/java/org/sonar/db/organization/OrganizationMemberDaoTest.java View File

@@ -20,6 +20,7 @@

package org.sonar.db.organization;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.ibatis.exceptions.PersistenceException;
@@ -29,6 +30,7 @@ import org.junit.rules.ExpectedException;
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;
@@ -56,6 +58,22 @@ public class OrganizationMemberDaoTest {
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();

+ 1
- 1
server/sonar-db-dao/src/test/java/org/sonar/db/user/GroupMembershipDaoTest.java View File

@@ -63,7 +63,7 @@ public class GroupMembershipDaoTest {
}

@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();

+ 138
- 3
server/sonar-server/src/main/java/org/sonar/server/organization/ws/SearchMembersAction.java View File

@@ -20,25 +20,74 @@

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)
@@ -47,6 +96,92 @@ public class SearchMembersAction implements OrganizationsWsAction {

@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);
}
}

+ 14
- 10
server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndex.java View File

@@ -24,8 +24,8 @@ import java.util.ArrayList;
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;
@@ -133,20 +133,24 @@ public class UserIndex {
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,
@@ -156,7 +160,7 @@ public class UserIndex {
.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);
}

+ 87
- 0
server/sonar-server/src/main/java/org/sonar/server/user/index/UserQuery.java View File

@@ -0,0 +1,87 @@
/*
* 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;
}
}
}

+ 3
- 1
server/sonar-server/src/main/java/org/sonar/server/user/ws/SearchAction.java View File

@@ -39,6 +39,7 @@ 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 org.sonar.server.user.index.UserQuery;

import static com.google.common.base.MoreObjects.firstNonNull;
import static org.sonar.server.es.SearchOptions.MAX_LIMIT;
@@ -78,7 +79,8 @@ public class SearchAction implements UsersWsAction {
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);

+ 17
- 0
server/sonar-server/src/main/resources/org/sonar/server/organization/ws/search_members-example.json View File

@@ -1,2 +1,19 @@
{
"users": [
{
"login": "ada.lovelace",
"name": "Ada Lovelace",
"avatar": "680b0001b4952664631511a81a4edc59"
},
{
"login": "grace.hopper",
"name": "Grace Hopper",
"avatar": "36d90b0b8cc8639e4960e46116dce02c"
}
],
"paging": {
"pageIndex": 1,
"pageSize": 50,
"total": 2
}
}

+ 301
- 3
server/sonar-server/src/test/java/org/sonar/server/organization/ws/SearchMembersActionTest.java View File

@@ -20,26 +20,324 @@

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);
}
}
}

+ 46
- 9
server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexTest.java View File

@@ -19,7 +19,6 @@
*/
package org.sonar.server.user.index;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -29,8 +28,11 @@ import org.junit.Test;
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;

@@ -46,6 +48,8 @@ public class UserIndexTest {

private UserIndex underTest;

private UserQuery.Builder userQuery = UserQuery.builder();

@Before
public void setUp() {
underTest = new UserIndex(esTester.client());
@@ -127,16 +131,49 @@ public class UserIndexTest {

@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) {

+ 82
- 0
sonar-ws/src/main/java/org/sonarqube/ws/client/organization/SearchMembersWsRequest.java View File

@@ -0,0 +1,82 @@
/*
* 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;
}
}

+ 15
- 0
sonar-ws/src/main/protobuf/ws-organizations.proto View File

@@ -20,6 +20,8 @@ syntax = "proto2";

package sonarqube.ws.organizations;

import "ws-commons.proto";

option java_package = "org.sonarqube.ws";
option java_outer_classname = "Organizations";
option optimize_for = SPEED;
@@ -39,6 +41,12 @@ message SearchWsResponse {
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;
@@ -47,3 +55,10 @@ message Organization {
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;
}

Loading…
Cancel
Save