From f96f99021d99319b935e8c620396743f7ce197a7 Mon Sep 17 00:00:00 2001 From: Julien Lancelot Date: Fri, 22 Sep 2017 17:34:34 +0200 Subject: [PATCH] SONAR-1330 Search for user allowed to edit single quality profile --- .../qualityprofile/QProfileEditUsersDao.java | 10 + .../QProfileEditUsersMapper.java | 6 + .../db/qualityprofile/SearchUsersQuery.java | 124 ++++++ .../db/qualityprofile/UserMembershipDto.java | 41 ++ .../QProfileEditUsersMapper.xml | 60 ++- .../QProfileEditUsersDaoTest.java | 161 ++++++++ .../server/issue/ws/FakeAvatarResolver.java | 32 ++ .../qualityprofile/ws/SearchUsersAction.java | 99 ++++- .../ws/search_users-example.json | 14 +- .../issue/ws/AvatarResolverImplTest.java | 7 + .../ws/SearchUsersActionTest.java | 371 +++++++++++++++++- .../QualityProfilesService.java | 19 + .../qualityprofile/SearchUsersRequest.java | 127 ++++++ .../main/protobuf/ws-qualityprofiles.proto | 14 + .../QualityProfilesServiceTest.java | 32 ++ .../org/sonarqube/tests/Category6Suite.java | 2 + .../QualityProfilesEditTest.java | 126 ++++++ 17 files changed, 1231 insertions(+), 14 deletions(-) create mode 100644 server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/SearchUsersQuery.java create mode 100644 server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/UserMembershipDto.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/issue/ws/FakeAvatarResolver.java create mode 100644 sonar-ws/src/main/java/org/sonarqube/ws/client/qualityprofile/SearchUsersRequest.java create mode 100644 tests/src/test/java/org/sonarqube/tests/qualityProfile/QualityProfilesEditTest.java diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditUsersDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditUsersDao.java index b173b067332..2cab9a929aa 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditUsersDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditUsersDao.java @@ -19,9 +19,11 @@ */ package org.sonar.db.qualityprofile; +import java.util.List; import org.sonar.api.utils.System2; import org.sonar.db.Dao; import org.sonar.db.DbSession; +import org.sonar.db.Pagination; import org.sonar.db.user.UserDto; public class QProfileEditUsersDao implements Dao { @@ -36,6 +38,14 @@ public class QProfileEditUsersDao implements Dao { return mapper(dbSession).selectByQProfileAndUser(profile.getKee(), user.getId()) != null; } + public int countByQuery(DbSession dbSession, SearchUsersQuery query){ + return mapper(dbSession).countByQuery(query); + } + + public List selectByQuery(DbSession dbSession, SearchUsersQuery query, Pagination pagination){ + return mapper(dbSession).selectByQuery(query, pagination); + } + public void insert(DbSession dbSession, QProfileEditUsersDto dto) { mapper(dbSession).insert(dto, system2.now()); } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditUsersMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditUsersMapper.java index 601b03791f8..70694c3307f 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditUsersMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditUsersMapper.java @@ -19,12 +19,18 @@ */ package org.sonar.db.qualityprofile; +import java.util.List; import org.apache.ibatis.annotations.Param; +import org.sonar.db.Pagination; public interface QProfileEditUsersMapper { QProfileEditUsersDto selectByQProfileAndUser(@Param("qProfileUuid") String qProfileUuid, @Param("userId") int userId); + int countByQuery(@Param("query") SearchUsersQuery query); + + List selectByQuery(@Param("query") SearchUsersQuery query, @Param("pagination") Pagination pagination); + void insert(@Param("dto") QProfileEditUsersDto dto, @Param("now") long now); void delete(@Param("qProfileUuid") String qProfileUuid, @Param("userId") int userId); diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/SearchUsersQuery.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/SearchUsersQuery.java new file mode 100644 index 00000000000..cdb6077c600 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/SearchUsersQuery.java @@ -0,0 +1,124 @@ +/* + * 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.db.qualityprofile; + +import com.google.common.collect.ImmutableSet; +import java.util.Locale; +import java.util.Set; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.apache.commons.lang.StringUtils; +import org.sonar.db.organization.OrganizationDto; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; +import static org.sonar.db.DaoDatabaseUtils.buildLikeValue; +import static org.sonar.db.WildcardPosition.BEFORE_AND_AFTER; + +public class SearchUsersQuery { + + public static final String ANY = "ANY"; + public static final String IN = "IN"; + public static final String OUT = "OUT"; + public static final Set AVAILABLE_MEMBERSHIPS = ImmutableSet.of(ANY, IN, OUT); + + private final String organizationUuid; + private final String qProfileUuid; + private final String query; + private final String membership; + + // for internal use in MyBatis + final String querySql; + final String querySqlLowercase; + + private SearchUsersQuery(Builder builder) { + this.organizationUuid = builder.organization.getUuid(); + this.qProfileUuid = builder.profile.getKee(); + this.query = builder.query; + this.membership = builder.membership; + this.querySql = query == null ? null : buildLikeValue(query, BEFORE_AND_AFTER); + this.querySqlLowercase = querySql == null ? null : querySql.toLowerCase(Locale.ENGLISH); + } + + public String getOrganizationUuid() { + return organizationUuid; + } + + public String getQProfileUuid() { + return qProfileUuid; + } + + public String getMembership() { + return membership; + } + + @CheckForNull + public String getQuery() { + return query; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private OrganizationDto organization; + private QProfileDto profile; + private String query; + private String membership; + + private Builder() { + } + + public Builder setOrganization(OrganizationDto organization) { + this.organization = organization; + return this; + } + + public Builder setProfile(QProfileDto profile) { + this.profile = profile; + return this; + } + + public Builder setMembership(@Nullable String membership) { + this.membership = membership; + return this; + } + + public Builder setQuery(@Nullable String s) { + this.query = StringUtils.defaultIfBlank(s, null); + return this; + } + + private void initMembership() { + membership = firstNonNull(membership, ANY); + checkArgument(AVAILABLE_MEMBERSHIPS.contains(membership), + "Membership is not valid (got " + membership + "). Availables values are " + AVAILABLE_MEMBERSHIPS); + } + + public SearchUsersQuery build() { + requireNonNull(organization, "Organization cannot be null"); + requireNonNull(profile, "Quality profile cant be null."); + initMembership(); + return new SearchUsersQuery(this); + } + } +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/UserMembershipDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/UserMembershipDto.java new file mode 100644 index 00000000000..7c301457135 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/UserMembershipDto.java @@ -0,0 +1,41 @@ +/* + * 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.db.qualityprofile; + +public class UserMembershipDto { + + private int userId; + // Set by MyBatis + private String uuid; + + public int getUserId() { + return userId; + } + + public UserMembershipDto setUserId(int userId) { + this.userId = userId; + return this; + } + + public boolean isSelected() { + return uuid != null; + } + +} diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QProfileEditUsersMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QProfileEditUsersMapper.xml index a56278ea9d8..8bf84485bc4 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QProfileEditUsersMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QProfileEditUsersMapper.xml @@ -18,12 +18,70 @@ and qeu.qprofile_uuid = #{qProfileUuid, jdbcType=VARCHAR} + + + + + + + + + + FROM users u + LEFT JOIN qprofile_edit_users qeu ON qeu.user_id=u.id AND qeu.qprofile_uuid=#{query.qProfileUuid, jdbcType=VARCHAR} + INNER JOIN organization_members om ON u.id=om.user_id AND om.organization_uuid=#{query.organizationUuid, jdbcType=VARCHAR} + + + + AND qeu.uuid IS NOT NULL + + + AND qeu.uuid IS NULL + + + + AND ( + lower(u.name) like #{query.querySqlLowercase} ESCAPE '/' + or u.login like #{query.querySql} ESCAPE '/') + + AND u.active=${_true} + + + insert into qprofile_edit_users( uuid, user_id, qprofile_uuid, - created_at, + created_at ) values ( #{dto.uuid, jdbcType=VARCHAR}, #{dto.userId, jdbcType=INTEGER}, diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QProfileEditUsersDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QProfileEditUsersDaoTest.java index 5f0992e9a8e..e0a90e7cbb4 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QProfileEditUsersDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QProfileEditUsersDaoTest.java @@ -24,11 +24,17 @@ import org.junit.Test; import org.sonar.api.utils.System2; import org.sonar.api.utils.internal.TestSystem2; import org.sonar.db.DbTester; +import org.sonar.db.Pagination; import org.sonar.db.organization.OrganizationDto; import org.sonar.db.user.UserDto; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; +import static org.assertj.core.api.Assertions.tuple; +import static org.sonar.db.qualityprofile.SearchUsersQuery.ANY; +import static org.sonar.db.qualityprofile.SearchUsersQuery.IN; +import static org.sonar.db.qualityprofile.SearchUsersQuery.OUT; +import static org.sonar.db.qualityprofile.SearchUsersQuery.builder; public class QProfileEditUsersDaoTest { @@ -56,6 +62,161 @@ public class QProfileEditUsersDaoTest { assertThat(underTest.exists(db.getSession(), anotherProfile, anotherUser)).isFalse(); } + @Test + public void countByQuery() { + OrganizationDto organization = db.organizations().insert(); + QProfileDto profile = db.qualityProfiles().insert(organization); + UserDto user1 = db.users().insertUser(); + UserDto user2 = db.users().insertUser(); + UserDto user3 = db.users().insertUser(); + db.organizations().addMember(organization, user1); + db.organizations().addMember(organization, user2); + db.organizations().addMember(organization, user3); + db.qualityProfiles().addUserPermission(profile, user1); + db.qualityProfiles().addUserPermission(profile, user2); + + assertThat(underTest.countByQuery(db.getSession(), builder() + .setOrganization(organization) + .setProfile(profile) + .setMembership(ANY).build())) + .isEqualTo(3); + + assertThat(underTest.countByQuery(db.getSession(), builder() + .setOrganization(organization) + .setProfile(profile) + .setMembership(IN).build())) + .isEqualTo(2); + + assertThat(underTest.countByQuery(db.getSession(), builder() + .setOrganization(organization) + .setProfile(profile) + .setMembership(OUT).build())) + .isEqualTo(1); + } + + @Test + public void selectByQuery() { + OrganizationDto organization = db.organizations().insert(); + QProfileDto profile = db.qualityProfiles().insert(organization); + UserDto user1 = db.users().insertUser(); + UserDto user2 = db.users().insertUser(); + UserDto user3 = db.users().insertUser(); + db.organizations().addMember(organization, user1); + db.organizations().addMember(organization, user2); + db.organizations().addMember(organization, user3); + db.qualityProfiles().addUserPermission(profile, user1); + db.qualityProfiles().addUserPermission(profile, user2); + + assertThat(underTest.selectByQuery(db.getSession(), builder() + .setOrganization(organization) + .setProfile(profile) + .setMembership(ANY).build(), Pagination.all())) + .extracting(UserMembershipDto::getUserId, UserMembershipDto::isSelected) + .containsExactlyInAnyOrder( + tuple(user1.getId(), true), + tuple(user2.getId(), true), + tuple(user3.getId(), false)); + + assertThat(underTest.selectByQuery(db.getSession(), builder() + .setOrganization(organization) + .setProfile(profile) + .setMembership(IN).build(), + Pagination.all())) + .extracting(UserMembershipDto::getUserId, UserMembershipDto::isSelected) + .containsExactlyInAnyOrder(tuple(user1.getId(), true), tuple(user2.getId(), true)); + + assertThat(underTest.selectByQuery(db.getSession(), builder() + .setOrganization(organization) + .setProfile(profile) + .setMembership(OUT).build(), + Pagination.all())) + .extracting(UserMembershipDto::getUserId, UserMembershipDto::isSelected) + .containsExactlyInAnyOrder(tuple(user3.getId(), false)); + } + + @Test + public void selectByQuery_search_by_name_or_login() { + OrganizationDto organization = db.organizations().insert(); + QProfileDto profile = db.qualityProfiles().insert(organization); + UserDto user1 = db.users().insertUser(u -> u.setLogin("user1").setName("John Doe")); + UserDto user2 = db.users().insertUser(u -> u.setLogin("user2").setName("John Smith")); + UserDto user3 = db.users().insertUser(u -> u.setLogin("user3").setName("Jane Doe")); + db.organizations().addMember(organization, user1); + db.organizations().addMember(organization, user2); + db.organizations().addMember(organization, user3); + db.qualityProfiles().addUserPermission(profile, user1); + db.qualityProfiles().addUserPermission(profile, user2); + db.qualityProfiles().addUserPermission(profile, user3); + + assertThat(underTest.selectByQuery(db.getSession(), builder() + .setOrganization(organization) + .setProfile(profile) + .setMembership(IN) + .setQuery("user2").build(), + Pagination.all())) + .extracting(UserMembershipDto::getUserId) + .containsExactlyInAnyOrder(user2.getId()); + + assertThat(underTest.selectByQuery(db.getSession(), builder() + .setOrganization(organization) + .setProfile(profile) + .setMembership(IN) + .setQuery("joh").build(), + Pagination.all())) + .extracting(UserMembershipDto::getUserId) + .containsExactlyInAnyOrder(user1.getId(), user2.getId()); + + assertThat(underTest.selectByQuery(db.getSession(), builder() + .setOrganization(organization) + .setProfile(profile) + .setMembership(IN) + .setQuery("Doe").build(), + Pagination.all())) + .extracting(UserMembershipDto::getUserId) + .containsExactlyInAnyOrder(user1.getId(), user3.getId()); + } + + @Test + public void selectByQuery_with_paging() { + OrganizationDto organization = db.organizations().insert(); + QProfileDto profile = db.qualityProfiles().insert(organization); + UserDto user1 = db.users().insertUser(u -> u.setName("user1")); + UserDto user2 = db.users().insertUser(u -> u.setName("user2")); + UserDto user3 = db.users().insertUser(u -> u.setName("user3")); + db.organizations().addMember(organization, user1); + db.organizations().addMember(organization, user2); + db.organizations().addMember(organization, user3); + db.qualityProfiles().addUserPermission(profile, user1); + db.qualityProfiles().addUserPermission(profile, user2); + + assertThat(underTest.selectByQuery(db.getSession(), builder() + .setOrganization(organization) + .setProfile(profile) + .setMembership(ANY) + .build(), + Pagination.forPage(1).andSize(1))) + .extracting(UserMembershipDto::getUserId) + .containsExactly(user1.getId()); + + assertThat(underTest.selectByQuery(db.getSession(), builder() + .setOrganization(organization) + .setProfile(profile) + .setMembership(ANY) + .build(), + Pagination.forPage(3).andSize(1))) + .extracting(UserMembershipDto::getUserId) + .containsExactly(user3.getId()); + + assertThat(underTest.selectByQuery(db.getSession(), builder() + .setOrganization(organization) + .setProfile(profile) + .setMembership(ANY) + .build(), + Pagination.forPage(1).andSize(10))) + .extracting(UserMembershipDto::getUserId) + .containsExactly(user1.getId(), user2.getId(), user3.getId()); + } + @Test public void insert() { underTest.insert(db.getSession(), new QProfileEditUsersDto() diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/FakeAvatarResolver.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/FakeAvatarResolver.java new file mode 100644 index 00000000000..c830925afe5 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/FakeAvatarResolver.java @@ -0,0 +1,32 @@ +/* + * 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.issue.ws; + +import org.sonar.db.user.UserDto; + +public class FakeAvatarResolver implements AvatarResolver { + + @Override + public String create(UserDto user) { + return user.getEmail() + "_avatar"; + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/SearchUsersAction.java b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/SearchUsersAction.java index 4accbeb4091..44945638c0f 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/SearchUsersAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/SearchUsersAction.java @@ -19,26 +19,65 @@ */ package org.sonar.server.qualityprofile.ws; +import com.google.common.collect.ImmutableMap; import java.util.Arrays; -import org.apache.commons.io.IOUtils; +import java.util.List; +import java.util.Map; import org.sonar.api.resources.Language; import org.sonar.api.resources.Languages; 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.SelectionMode; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.qualityprofile.QProfileDto; +import org.sonar.db.qualityprofile.SearchUsersQuery; +import org.sonar.db.qualityprofile.UserMembershipDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.issue.ws.AvatarResolver; +import org.sonarqube.ws.Common; +import org.sonarqube.ws.QualityProfiles.SearchUsersResponse; +import org.sonarqube.ws.client.qualityprofile.SearchUsersRequest; +import static org.sonar.api.server.ws.WebService.Param.PAGE; +import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE; +import static org.sonar.api.server.ws.WebService.Param.SELECTED; +import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY; +import static org.sonar.api.server.ws.WebService.SelectionMode.ALL; +import static org.sonar.api.server.ws.WebService.SelectionMode.DESELECTED; +import static org.sonar.api.server.ws.WebService.SelectionMode.fromParam; +import static org.sonar.core.util.Protobuf.setNullable; +import static org.sonar.core.util.stream.MoreCollectors.toList; import static org.sonar.core.util.stream.MoreCollectors.toSet; +import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; +import static org.sonar.db.Pagination.forPage; +import static org.sonar.db.qualityprofile.SearchUsersQuery.ANY; +import static org.sonar.db.qualityprofile.SearchUsersQuery.IN; +import static org.sonar.db.qualityprofile.SearchUsersQuery.OUT; +import static org.sonar.db.qualityprofile.SearchUsersQuery.builder; import static org.sonar.server.qualityprofile.ws.QProfileWsSupport.createOrganizationParam; +import static org.sonar.server.ws.WsUtils.writeProtobuf; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.ACTION_SEARCH_USERS; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_LANGUAGE; +import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_ORGANIZATION; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_QUALITY_PROFILE; public class SearchUsersAction implements QProfileWsAction { + private static final Map MEMBERSHIP = ImmutableMap.of(SelectionMode.SELECTED, IN, DESELECTED, OUT, ALL, ANY); + + private final DbClient dbClient; + private final QProfileWsSupport wsSupport; private final Languages languages; + private final AvatarResolver avatarResolver; - public SearchUsersAction(Languages languages) { + public SearchUsersAction(DbClient dbClient, QProfileWsSupport wsSupport, Languages languages, AvatarResolver avatarResolver) { + this.dbClient = dbClient; + this.wsSupport = wsSupport; this.languages = languages; + this.avatarResolver = avatarResolver; } @Override @@ -71,6 +110,60 @@ public class SearchUsersAction implements QProfileWsAction { @Override public void handle(Request request, Response response) throws Exception { - IOUtils.write(IOUtils.toString(getClass().getResource("search_users-example.json")), response.stream().output()); + SearchUsersRequest wsRequest = buildRequest(request); + try (DbSession dbSession = dbClient.openSession(false)) { + OrganizationDto organization = wsSupport.getOrganizationByKey(dbSession, wsRequest.getOrganization()); + QProfileDto profile = wsSupport.getProfile(dbSession, organization, wsRequest.getQualityProfile(), wsRequest.getLanguage()); + wsSupport.checkCanEdit(dbSession, profile); + + SearchUsersQuery query = builder() + .setOrganization(organization) + .setProfile(profile) + .setQuery(wsRequest.getQuery()) + .setMembership(MEMBERSHIP.get(fromParam(wsRequest.getSelected()))) + .build(); + int total = dbClient.qProfileEditUsersDao().countByQuery(dbSession, query); + List usersMembership = dbClient.qProfileEditUsersDao().selectByQuery(dbSession, query, + forPage(wsRequest.getPage()).andSize(wsRequest.getPageSize())); + Map usersById = dbClient.userDao().selectByIds(dbSession, usersMembership.stream().map(UserMembershipDto::getUserId).collect(toList())) + .stream().collect(uniqueIndex(UserDto::getId)); + writeProtobuf( + SearchUsersResponse.newBuilder() + .addAllUsers(usersMembership.stream() + .map(userMembershipDto -> toUser(usersById.get(userMembershipDto.getUserId()), userMembershipDto.isSelected())) + .collect(toList())) + .setPaging(buildPaging(wsRequest, total)).build(), request, response); + } } + + private static SearchUsersRequest buildRequest(Request request) { + return SearchUsersRequest.builder() + .setOrganization(request.param(PARAM_ORGANIZATION)) + .setQualityProfile(request.mandatoryParam(PARAM_QUALITY_PROFILE)) + .setLanguage(request.mandatoryParam(PARAM_LANGUAGE)) + .setQuery(request.param(TEXT_QUERY)) + .setSelected(request.mandatoryParam(SELECTED)) + .setPage(request.mandatoryParamAsInt(PAGE)) + .setPageSize(request.mandatoryParamAsInt(PAGE_SIZE)) + .build(); + } + + private SearchUsersResponse.User toUser(UserDto user, boolean isSelected) { + SearchUsersResponse.User.Builder builder = SearchUsersResponse.User.newBuilder() + .setLogin(user.getLogin()) + .setName(user.getName()) + .setSelected(isSelected); + setNullable(user.getEmail(), e -> builder.setAvatar(avatarResolver.create(user))); + return builder + .build(); + } + + private static Common.Paging buildPaging(SearchUsersRequest wsRequest, int total) { + return Common.Paging.newBuilder() + .setPageIndex(wsRequest.getPage()) + .setPageSize(wsRequest.getPageSize()) + .setTotal(total) + .build(); + } + } diff --git a/server/sonar-server/src/main/resources/org/sonar/server/qualityprofile/ws/search_users-example.json b/server/sonar-server/src/main/resources/org/sonar/server/qualityprofile/ws/search_users-example.json index 2eb7a636830..b6f08a35742 100644 --- a/server/sonar-server/src/main/resources/org/sonar/server/qualityprofile/ws/search_users-example.json +++ b/server/sonar-server/src/main/resources/org/sonar/server/qualityprofile/ws/search_users-example.json @@ -1,19 +1,21 @@ { + "paging": { + "pageSize": 25, + "total": 2, + "pageIndex": 1 + }, "users": [ { "login": "admin", "name": "Administrator", + "avatar": "59235f35e4763abb0b547bd093562f6e", "selected": true }, { "login": "george.orwell", "name": "George Orwell", + "avatar": "61a6fd6cba04e9394b35f349c9ca3380", "selected": false } - ], - "paging": { - "pageSize": 100, - "total": 2, - "pageIndex": 1 - } + ] } diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AvatarResolverImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AvatarResolverImplTest.java index c2b932c50e8..bd3e5c2bb9f 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AvatarResolverImplTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AvatarResolverImplTest.java @@ -41,6 +41,13 @@ public class AvatarResolverImplTest { assertThat(avatar).isEqualTo("9297bfb538f650da6143b604e82a355d"); } + @Test + public void create_when_empty_email() throws Exception { + String avatar = underTest.create(newUserDto("john", "John", "")); + + assertThat(avatar).isEqualTo("d41d8cd98f00b204e9800998ecf8427e"); + } + @Test public void create_is_case_insensitive() throws Exception { assertThat(underTest.create(newUserDto("john", "John", "john@doo.com"))).isEqualTo(underTest.create(newUserDto("john", "John", "John@Doo.com"))); diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/SearchUsersActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/SearchUsersActionTest.java index 7b28d74bdf6..bcaf668c046 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/SearchUsersActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/SearchUsersActionTest.java @@ -21,25 +21,56 @@ package org.sonar.server.qualityprofile.ws; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.sonar.api.resources.Languages; import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbTester; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.permission.OrganizationPermission; +import org.sonar.db.qualityprofile.QProfileDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.issue.ws.AvatarResolver; +import org.sonar.server.issue.ws.AvatarResolverImpl; +import org.sonar.server.issue.ws.FakeAvatarResolver; import org.sonar.server.language.LanguageTesting; +import org.sonar.server.organization.TestDefaultOrganizationProvider; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.WsActionTester; +import org.sonarqube.ws.QualityProfiles.SearchUsersResponse; +import static java.lang.String.format; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.sonar.api.server.ws.WebService.Param.PAGE; +import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE; +import static org.sonar.api.server.ws.WebService.Param.SELECTED; +import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY; +import static org.sonar.db.permission.OrganizationPermission.ADMINISTER_QUALITY_PROFILES; import static org.sonar.test.JsonAssert.assertJson; import static org.sonarqube.ws.MediaTypes.JSON; +import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_LANGUAGE; +import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_ORGANIZATION; +import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_QUALITY_PROFILE; public class SearchUsersActionTest { private static final String XOO = "xoo"; - private static final Languages LANGUAGES = LanguageTesting.newLanguages(XOO); + private static final String FOO = "foo"; + private static final Languages LANGUAGES = LanguageTesting.newLanguages(XOO, FOO); + @Rule + public ExpectedException expectedException = ExpectedException.none(); @Rule public UserSessionRule userSession = UserSessionRule.standalone(); + @Rule + public DbTester db = DbTester.create(); + + private QProfileWsSupport wsSupport = new QProfileWsSupport(db.getDbClient(), userSession, TestDefaultOrganizationProvider.from(db)); + private AvatarResolver avatarResolver = new FakeAvatarResolver(); - private WsActionTester ws = new WsActionTester(new SearchUsersAction(LANGUAGES)); + private WsActionTester ws = new WsActionTester(new SearchUsersAction(db.getDbClient(), wsSupport, LANGUAGES, avatarResolver)); @Test public void test_definition() { @@ -53,8 +84,340 @@ public class SearchUsersActionTest { @Test public void test_example() { - String result = ws.newRequest().setMediaType(JSON).execute().getInput(); + avatarResolver = new AvatarResolverImpl(); + ws = new WsActionTester(new SearchUsersAction(db.getDbClient(), wsSupport, LANGUAGES, avatarResolver)); + OrganizationDto organization = db.organizations().insert(); + QProfileDto profile = db.qualityProfiles().insert(organization, p -> p.setLanguage(XOO)); + UserDto user1 = db.users().insertUser(u -> u.setLogin("admin").setName("Administrator").setEmail("admin@email.com")); + UserDto user2 = db.users().insertUser(u -> u.setLogin("george.orwell").setName("George Orwell").setEmail("george@orwell.com")); + db.organizations().addMember(organization, user1); + db.organizations().addMember(organization, user2); + db.qualityProfiles().addUserPermission(profile, user1); + userSession.logIn().addPermission(ADMINISTER_QUALITY_PROFILES, organization); + + String result = ws.newRequest() + .setParam(PARAM_ORGANIZATION, organization.getKey()) + .setParam(PARAM_QUALITY_PROFILE, profile.getName()) + .setParam(PARAM_LANGUAGE, XOO) + .setParam(SELECTED, "all") + .setMediaType(JSON) + .execute() + .getInput(); + + assertJson(result).isSimilarTo(ws.getDef().responseExampleAsString()); + } + + @Test + public void search_all_users() { + OrganizationDto organization = db.organizations().insert(); + QProfileDto profile = db.qualityProfiles().insert(organization, p -> p.setLanguage(XOO)); + UserDto user1 = db.users().insertUser(u -> u.setEmail("user1@email.com")); + UserDto user2 = db.users().insertUser(u -> u.setEmail("user2@email.com")); + db.organizations().addMember(organization, user1); + db.organizations().addMember(organization, user2); + db.qualityProfiles().addUserPermission(profile, user1); + userSession.logIn().addPermission(ADMINISTER_QUALITY_PROFILES, organization); + + SearchUsersResponse response = ws.newRequest() + .setParam(PARAM_ORGANIZATION, organization.getKey()) + .setParam(PARAM_QUALITY_PROFILE, profile.getName()) + .setParam(PARAM_LANGUAGE, XOO) + .setParam(SELECTED, "all") + .executeProtobuf(SearchUsersResponse.class); + + assertThat(response.getUsersList()).extracting(SearchUsersResponse.User::getLogin, SearchUsersResponse.User::getName, SearchUsersResponse.User::getAvatar, SearchUsersResponse.User::getSelected) + .containsExactlyInAnyOrder( + tuple(user1.getLogin(), user1.getName(), "user1@email.com_avatar", true), + tuple(user2.getLogin(), user2.getName(), "user2@email.com_avatar", false)); + } + + @Test + public void search_selected_users() { + OrganizationDto organization = db.organizations().insert(); + QProfileDto profile = db.qualityProfiles().insert(organization, p -> p.setLanguage(XOO)); + UserDto user1 = db.users().insertUser(); + UserDto user2 = db.users().insertUser(); + db.organizations().addMember(organization, user1); + db.organizations().addMember(organization, user2); + db.qualityProfiles().addUserPermission(profile, user1); + userSession.logIn().addPermission(ADMINISTER_QUALITY_PROFILES, organization); + + SearchUsersResponse response = ws.newRequest() + .setParam(PARAM_ORGANIZATION, organization.getKey()) + .setParam(PARAM_QUALITY_PROFILE, profile.getName()) + .setParam(PARAM_LANGUAGE, XOO) + .setParam(SELECTED, "selected") + .executeProtobuf(SearchUsersResponse.class); + + assertThat(response.getUsersList()).extracting(SearchUsersResponse.User::getLogin, SearchUsersResponse.User::getName, SearchUsersResponse.User::getSelected) + .containsExactlyInAnyOrder( + tuple(user1.getLogin(), user1.getName(), true)); + } + + @Test + public void search_deselected_users() { + OrganizationDto organization = db.organizations().insert(); + QProfileDto profile = db.qualityProfiles().insert(organization, p -> p.setLanguage(XOO)); + UserDto user1 = db.users().insertUser(); + UserDto user2 = db.users().insertUser(); + db.organizations().addMember(organization, user1); + db.organizations().addMember(organization, user2); + db.qualityProfiles().addUserPermission(profile, user1); + userSession.logIn().addPermission(ADMINISTER_QUALITY_PROFILES, organization); + + SearchUsersResponse response = ws.newRequest() + .setParam(PARAM_ORGANIZATION, organization.getKey()) + .setParam(PARAM_QUALITY_PROFILE, profile.getName()) + .setParam(PARAM_LANGUAGE, XOO) + .setParam(SELECTED, "deselected") + .executeProtobuf(SearchUsersResponse.class); + + assertThat(response.getUsersList()).extracting(SearchUsersResponse.User::getLogin, SearchUsersResponse.User::getName, SearchUsersResponse.User::getSelected) + .containsExactlyInAnyOrder( + tuple(user2.getLogin(), user2.getName(), false)); + } + + @Test + public void search_by_login() { + OrganizationDto organization = db.organizations().insert(); + QProfileDto profile = db.qualityProfiles().insert(organization, p -> p.setLanguage(XOO)); + UserDto user1 = db.users().insertUser(); + UserDto user2 = db.users().insertUser(); + db.organizations().addMember(organization, user1); + db.organizations().addMember(organization, user2); + db.qualityProfiles().addUserPermission(profile, user1); + userSession.logIn().addPermission(ADMINISTER_QUALITY_PROFILES, organization); + + SearchUsersResponse response = ws.newRequest() + .setParam(PARAM_ORGANIZATION, organization.getKey()) + .setParam(PARAM_QUALITY_PROFILE, profile.getName()) + .setParam(PARAM_LANGUAGE, XOO) + .setParam(TEXT_QUERY, user1.getLogin()) + .setParam(SELECTED, "all") + .executeProtobuf(SearchUsersResponse.class); + + assertThat(response.getUsersList()).extracting(SearchUsersResponse.User::getLogin) + .containsExactlyInAnyOrder(user1.getLogin()); + } + + @Test + public void search_by_name() { + OrganizationDto organization = db.organizations().insert(); + QProfileDto profile = db.qualityProfiles().insert(organization, p -> p.setLanguage(XOO)); + UserDto user1 = db.users().insertUser(u -> u.setName("John Doe")); + UserDto user2 = db.users().insertUser(u -> u.setName("Jane Doe")); + UserDto user3 = db.users().insertUser(u -> u.setName("John Smith")); + db.organizations().addMember(organization, user1); + db.organizations().addMember(organization, user2); + db.organizations().addMember(organization, user3); + db.qualityProfiles().addUserPermission(profile, user1); + userSession.logIn().addPermission(ADMINISTER_QUALITY_PROFILES, organization); + + SearchUsersResponse response = ws.newRequest() + .setParam(PARAM_ORGANIZATION, organization.getKey()) + .setParam(PARAM_QUALITY_PROFILE, profile.getName()) + .setParam(PARAM_LANGUAGE, XOO) + .setParam(TEXT_QUERY, "ohn") + .setParam(SELECTED, "all") + .executeProtobuf(SearchUsersResponse.class); + + assertThat(response.getUsersList()).extracting(SearchUsersResponse.User::getLogin) + .containsExactlyInAnyOrder(user1.getLogin(), user3.getLogin()); + } + + @Test + public void user_without_email() { + OrganizationDto organization = db.organizations().insert(); + QProfileDto profile = db.qualityProfiles().insert(organization, p -> p.setLanguage(XOO)); + UserDto user = db.users().insertUser(u -> u.setEmail(null)); + db.organizations().addMember(organization, user); + db.qualityProfiles().addUserPermission(profile, user); + userSession.logIn().addPermission(ADMINISTER_QUALITY_PROFILES, organization); + + SearchUsersResponse response = ws.newRequest() + .setParam(PARAM_ORGANIZATION, organization.getKey()) + .setParam(PARAM_QUALITY_PROFILE, profile.getName()) + .setParam(PARAM_LANGUAGE, XOO) + .setParam(SELECTED, "all") + .executeProtobuf(SearchUsersResponse.class); + + assertThat(response.getUsersList()).extracting(SearchUsersResponse.User::getLogin, SearchUsersResponse.User::hasAvatar) + .containsExactlyInAnyOrder(tuple(user.getLogin(), false)); + } + + @Test + public void paging_search() { + OrganizationDto organization = db.organizations().insert(); + QProfileDto profile = db.qualityProfiles().insert(organization, p -> p.setLanguage(XOO)); + UserDto user2 = db.users().insertUser(u -> u.setName("user2")); + UserDto user3 = db.users().insertUser(u -> u.setName("user3")); + UserDto user1 = db.users().insertUser(u -> u.setName("user1")); + db.organizations().addMember(organization, user1); + db.organizations().addMember(organization, user2); + db.organizations().addMember(organization, user3); + db.qualityProfiles().addUserPermission(profile, user1); + db.qualityProfiles().addUserPermission(profile, user2); + userSession.logIn().addPermission(ADMINISTER_QUALITY_PROFILES, organization); + + assertThat(ws.newRequest() + .setParam(PARAM_ORGANIZATION, organization.getKey()) + .setParam(PARAM_QUALITY_PROFILE, profile.getName()) + .setParam(PARAM_LANGUAGE, XOO) + .setParam(SELECTED, "all") + .setParam(PAGE, "1") + .setParam(PAGE_SIZE, "1") + .executeProtobuf(SearchUsersResponse.class).getUsersList()) + .extracting(SearchUsersResponse.User::getLogin) + .containsExactly(user1.getLogin()); + + assertThat(ws.newRequest() + .setParam(PARAM_ORGANIZATION, organization.getKey()) + .setParam(PARAM_QUALITY_PROFILE, profile.getName()) + .setParam(PARAM_LANGUAGE, XOO) + .setParam(SELECTED, "all") + .setParam(PAGE, "3") + .setParam(PAGE_SIZE, "1") + .executeProtobuf(SearchUsersResponse.class).getUsersList()) + .extracting(SearchUsersResponse.User::getLogin) + .containsExactly(user3.getLogin()); + + assertThat(ws.newRequest() + .setParam(PARAM_ORGANIZATION, organization.getKey()) + .setParam(PARAM_QUALITY_PROFILE, profile.getName()) + .setParam(PARAM_LANGUAGE, XOO) + .setParam(SELECTED, "all") + .setParam(PAGE, "1") + .setParam(PAGE_SIZE, "10") + .executeProtobuf(SearchUsersResponse.class).getUsersList()) + .extracting(SearchUsersResponse.User::getLogin) + .containsExactly(user1.getLogin(), user2.getLogin(), user3.getLogin()); + } + + @Test + public void uses_default_organization_when_no_organization() { + OrganizationDto organization = db.getDefaultOrganization(); + QProfileDto profile = db.qualityProfiles().insert(organization, p -> p.setLanguage(XOO)); + UserDto user1 = db.users().insertUser(); + db.organizations().addMember(organization, user1); + db.qualityProfiles().addUserPermission(profile, user1); + userSession.logIn().addPermission(ADMINISTER_QUALITY_PROFILES, organization); + + SearchUsersResponse response = ws.newRequest() + .setParam(PARAM_QUALITY_PROFILE, profile.getName()) + .setParam(PARAM_LANGUAGE, XOO) + .setParam(SELECTED, "all") + .executeProtobuf(SearchUsersResponse.class); + + assertThat(response.getUsersList()).extracting(SearchUsersResponse.User::getLogin).containsExactlyInAnyOrder(user1.getLogin()); + } + + @Test + public void qp_administers_can_search_users() { + OrganizationDto organization = db.organizations().insert(); + QProfileDto profile = db.qualityProfiles().insert(organization, p -> p.setLanguage(XOO)); + UserDto user = db.users().insertUser(); + db.organizations().addMember(organization, user); + userSession.logIn().addPermission(OrganizationPermission.ADMINISTER_QUALITY_PROFILES, organization); + + SearchUsersResponse response = ws.newRequest() + .setParam(PARAM_ORGANIZATION, organization.getKey()) + .setParam(PARAM_QUALITY_PROFILE, profile.getName()) + .setParam(PARAM_LANGUAGE, XOO) + .setParam(SELECTED, "all") + .executeProtobuf(SearchUsersResponse.class); + + assertThat(response.getUsersList()).extracting(SearchUsersResponse.User::getLogin).containsExactlyInAnyOrder(user.getLogin()); + } + + @Test + public void qp_editors_can_search_users() { + OrganizationDto organization = db.organizations().insert(); + QProfileDto profile = db.qualityProfiles().insert(organization, p -> p.setLanguage(XOO)); + UserDto user = db.users().insertUser(); + db.organizations().addMember(organization, user); + UserDto userAllowedToEditProfile = db.users().insertUser(); + db.qualityProfiles().addUserPermission(profile, userAllowedToEditProfile); + userSession.logIn(userAllowedToEditProfile); + + SearchUsersResponse response = ws.newRequest() + .setParam(PARAM_ORGANIZATION, organization.getKey()) + .setParam(PARAM_QUALITY_PROFILE, profile.getName()) + .setParam(PARAM_LANGUAGE, XOO) + .setParam(SELECTED, "all") + .executeProtobuf(SearchUsersResponse.class); + + assertThat(response.getUsersList()).extracting(SearchUsersResponse.User::getLogin).containsExactlyInAnyOrder(user.getLogin()); + } + + @Test + public void fail_when_qprofile_does_not_exist() { + OrganizationDto organization = db.organizations().insert(); + UserDto user = db.users().insertUser(); + db.organizations().addMember(organization, user); + userSession.logIn().addPermission(OrganizationPermission.ADMINISTER_QUALITY_PROFILES, organization); + + expectedException.expect(NotFoundException.class); + expectedException.expectMessage(format("Quality Profile for language 'xoo' and name 'unknown' does not exist in organization '%s'", organization.getKey())); + + ws.newRequest() + .setParam(PARAM_QUALITY_PROFILE, "unknown") + .setParam(PARAM_LANGUAGE, XOO) + .setParam(PARAM_ORGANIZATION, organization.getKey()) + .execute(); + } + + @Test + public void fail_when_qprofile_does_not_belong_to_organization() { + OrganizationDto organization = db.organizations().insert(); + UserDto user = db.users().insertUser(); + db.organizations().addMember(organization, user); + OrganizationDto anotherOrganization = db.organizations().insert(); + QProfileDto profile = db.qualityProfiles().insert(anotherOrganization, p -> p.setLanguage(XOO)); + userSession.logIn().addPermission(OrganizationPermission.ADMINISTER_QUALITY_PROFILES, organization); + + expectedException.expect(NotFoundException.class); + expectedException.expectMessage(format("Quality Profile for language 'xoo' and name '%s' does not exist in organization '%s'", profile.getName(), organization.getKey())); + + ws.newRequest() + .setParam(PARAM_QUALITY_PROFILE, profile.getName()) + .setParam(PARAM_LANGUAGE, XOO) + .setParam(PARAM_ORGANIZATION, organization.getKey()) + .execute(); + } + + @Test + public void fail_when_wrong_language() { + OrganizationDto organization = db.organizations().insert(); + QProfileDto profile = db.qualityProfiles().insert(organization, p -> p.setLanguage(XOO)); + UserDto user1 = db.users().insertUser(); + db.organizations().addMember(organization, user1); + db.qualityProfiles().addUserPermission(profile, user1); + userSession.logIn().addPermission(ADMINISTER_QUALITY_PROFILES, organization); + + expectedException.expect(NotFoundException.class); + expectedException.expectMessage(format("Quality Profile for language 'foo' and name '%s' does not exist in organization '%s'", profile.getName(), organization.getKey())); + + ws.newRequest() + .setParam(PARAM_ORGANIZATION, organization.getKey()) + .setParam(PARAM_QUALITY_PROFILE, profile.getName()) + .setParam(PARAM_LANGUAGE, FOO) + .executeProtobuf(SearchUsersResponse.class); + } + + @Test + public void fail_when_not_enough_permission() { + OrganizationDto organization = db.organizations().insert(); + QProfileDto profile = db.qualityProfiles().insert(organization, p -> p.setLanguage(XOO)); + UserDto user = db.users().insertUser(); + db.organizations().addMember(organization, user); + userSession.logIn(db.users().insertUser()).addPermission(OrganizationPermission.ADMINISTER_QUALITY_GATES, organization); + + expectedException.expect(ForbiddenException.class); - assertJson(ws.getDef().responseExampleAsString()).isSimilarTo(result); + ws.newRequest() + .setParam(PARAM_QUALITY_PROFILE, profile.getName()) + .setParam(PARAM_LANGUAGE, XOO) + .setParam(PARAM_ORGANIZATION, organization.getKey()) + .execute(); } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/qualityprofile/QualityProfilesService.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/qualityprofile/QualityProfilesService.java index 340fd30d4a5..bc95c27b7b6 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/qualityprofile/QualityProfilesService.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/qualityprofile/QualityProfilesService.java @@ -23,6 +23,7 @@ import org.sonarqube.ws.MediaTypes; import org.sonarqube.ws.QualityProfiles; import org.sonarqube.ws.QualityProfiles.CopyWsResponse; import org.sonarqube.ws.QualityProfiles.CreateWsResponse; +import org.sonarqube.ws.QualityProfiles.SearchUsersResponse; import org.sonarqube.ws.QualityProfiles.SearchWsResponse; import org.sonarqube.ws.QualityProfiles.ShowResponse; import org.sonarqube.ws.client.BaseService; @@ -30,6 +31,10 @@ import org.sonarqube.ws.client.GetRequest; import org.sonarqube.ws.client.PostRequest; import org.sonarqube.ws.client.WsConnector; +import static org.sonar.api.server.ws.WebService.Param.PAGE; +import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE; +import static org.sonar.api.server.ws.WebService.Param.SELECTED; +import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.ACTION_ACTIVATE_RULE; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.ACTION_ADD_PROJECT; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.ACTION_ADD_USER; @@ -42,6 +47,7 @@ import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters. import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.ACTION_REMOVE_USER; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.ACTION_RESTORE; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.ACTION_SEARCH; +import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.ACTION_SEARCH_USERS; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.ACTION_SET_DEFAULT; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.ACTION_SHOW; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.CONTROLLER_QUALITY_PROFILES; @@ -190,4 +196,17 @@ public class QualityProfilesService extends BaseService { .setParam(PARAM_LANGUAGE, request.getLanguage()) .setParam(PARAM_LOGIN, request.getUserLogin())); } + + public SearchUsersResponse searchUsers(SearchUsersRequest request) { + return call( + new GetRequest(path(ACTION_SEARCH_USERS)) + .setParam(PARAM_ORGANIZATION, request.getOrganization()) + .setParam(PARAM_QUALITY_PROFILE, request.getQualityProfile()) + .setParam(PARAM_LANGUAGE, request.getLanguage()) + .setParam(TEXT_QUERY, request.getQuery()) + .setParam(SELECTED, request.getSelected()) + .setParam(PAGE, request.getPage()) + .setParam(PAGE_SIZE, request.getPageSize()), + SearchUsersResponse.parser()); + } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/qualityprofile/SearchUsersRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/qualityprofile/SearchUsersRequest.java new file mode 100644 index 00000000000..247efbdb20c --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/qualityprofile/SearchUsersRequest.java @@ -0,0 +1,127 @@ +/* + * 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.qualityprofile; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; + +public class SearchUsersRequest { + + private String organization; + private String qualityProfile; + private String language; + private String selected; + private String query; + private Integer page; + private Integer pageSize; + + private SearchUsersRequest(Builder builder) { + this.organization = builder.organization; + this.qualityProfile = builder.qualityProfile; + this.language = builder.language; + this.selected = builder.selected; + this.query = builder.query; + this.page = builder.page; + this.pageSize = builder.pageSize; + } + + @CheckForNull + public String getOrganization() { + return organization; + } + + public String getQualityProfile() { + return qualityProfile; + } + + public String getLanguage() { + return language; + } + + @CheckForNull + public String getQuery() { + return query; + } + + public String getSelected() { + return selected; + } + + public Integer getPage() { + return page; + } + + public Integer getPageSize() { + return pageSize; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String organization; + private String qualityProfile; + private String language; + private String selected; + private String query; + private Integer page; + private Integer pageSize; + + public Builder setOrganization(@Nullable String organization) { + this.organization = organization; + return this; + } + + public Builder setQualityProfile(String qualityProfile) { + this.qualityProfile = qualityProfile; + return this; + } + + public Builder setLanguage(String language) { + this.language = language; + return this; + } + + public Builder setSelected(String selected) { + this.selected = selected; + return this; + } + + public Builder setQuery(@Nullable String query) { + this.query = query; + return this; + } + + public Builder setPage(Integer page) { + this.page = page; + return this; + } + + public Builder setPageSize(Integer pageSize) { + this.pageSize = pageSize; + return this; + } + + public SearchUsersRequest build() { + return new SearchUsersRequest(this); + } + } +} diff --git a/sonar-ws/src/main/protobuf/ws-qualityprofiles.proto b/sonar-ws/src/main/protobuf/ws-qualityprofiles.proto index a02f33fe395..2300e8baa77 100644 --- a/sonar-ws/src/main/protobuf/ws-qualityprofiles.proto +++ b/sonar-ws/src/main/protobuf/ws-qualityprofiles.proto @@ -20,6 +20,8 @@ syntax = "proto2"; package sonarqube.ws.qualityprofiles; +import "ws-commons.proto"; + option java_package = "org.sonarqube.ws"; option java_outer_classname = "QualityProfiles"; option optimize_for = SPEED; @@ -138,3 +140,15 @@ message ShowResponse { } } +// WS api/qualityprofiles/search_users +message SearchUsersResponse { + optional sonarqube.ws.commons.Paging paging = 1; + repeated User users = 2; + + message User { + optional string login = 1; + optional string name = 2; + optional string avatar = 3; + optional bool selected = 4; + } +} diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/qualityprofile/QualityProfilesServiceTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/qualityprofile/QualityProfilesServiceTest.java index 63c4fcc8e8d..655686b3e07 100644 --- a/sonar-ws/src/test/java/org/sonarqube/ws/client/qualityprofile/QualityProfilesServiceTest.java +++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/qualityprofile/QualityProfilesServiceTest.java @@ -22,6 +22,7 @@ package org.sonarqube.ws.client.qualityprofile; import org.junit.Rule; import org.junit.Test; import org.sonarqube.ws.Common.Severity; +import org.sonarqube.ws.QualityProfiles; import org.sonarqube.ws.QualityProfiles.SearchWsResponse; import org.sonarqube.ws.QualityProfiles.ShowResponse; import org.sonarqube.ws.client.GetRequest; @@ -31,6 +32,10 @@ import org.sonarqube.ws.client.WsConnector; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; +import static org.sonar.api.server.ws.WebService.Param.PAGE; +import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE; +import static org.sonar.api.server.ws.WebService.Param.SELECTED; +import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_COMPARE_TO_SONAR_WAY; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_DEFAULTS; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_FROM_KEY; @@ -235,4 +240,31 @@ public class QualityProfilesServiceTest { .hasParam(PARAM_LOGIN, "john") .andNoOtherParam(); } + + @Test + public void search_users() { + underTest.searchUsers(SearchUsersRequest.builder() + .setOrganization("O1") + .setQualityProfile("P1") + .setLanguage("Xoo") + .setQuery("john") + .setSelected("all") + .setPage(5) + .setPageSize(50) + .build() + ); + GetRequest getRequest = serviceTester.getGetRequest(); + + assertThat(serviceTester.getGetParser()).isSameAs(QualityProfiles.SearchUsersResponse.parser()); + serviceTester.assertThat(getRequest) + .hasPath("search_users") + .hasParam(PARAM_ORGANIZATION, "O1") + .hasParam(PARAM_QUALITY_PROFILE, "P1") + .hasParam(PARAM_LANGUAGE, "Xoo") + .hasParam(TEXT_QUERY, "john") + .hasParam(SELECTED, "all") + .hasParam(PAGE, 5) + .hasParam(PAGE_SIZE, 50) + .andNoOtherParam(); + } } diff --git a/tests/src/test/java/org/sonarqube/tests/Category6Suite.java b/tests/src/test/java/org/sonarqube/tests/Category6Suite.java index 660ff241cd1..8fc6f4f49b5 100644 --- a/tests/src/test/java/org/sonarqube/tests/Category6Suite.java +++ b/tests/src/test/java/org/sonarqube/tests/Category6Suite.java @@ -47,6 +47,7 @@ import org.sonarqube.tests.qualityGate.OrganizationQualityGateUiTest; import org.sonarqube.tests.qualityProfile.BuiltInQualityProfilesTest; import org.sonarqube.tests.qualityProfile.CustomQualityProfilesTest; import org.sonarqube.tests.qualityProfile.OrganizationQualityProfilesUiTest; +import org.sonarqube.tests.qualityProfile.QualityProfilesEditTest; import org.sonarqube.tests.qualityProfile.QualityProfilesWsTest; import org.sonarqube.tests.rule.RulesWsTest; import org.sonarqube.tests.ui.OrganizationUiExtensionsTest; @@ -72,6 +73,7 @@ import static util.ItUtils.xooPlugin; OrganizationUiExtensionsTest.class, PersonalOrganizationTest.class, BuiltInQualityProfilesTest.class, + QualityProfilesEditTest.class, QualityProfilesWsTest.class, CustomQualityProfilesTest.class, BillingTest.class, diff --git a/tests/src/test/java/org/sonarqube/tests/qualityProfile/QualityProfilesEditTest.java b/tests/src/test/java/org/sonarqube/tests/qualityProfile/QualityProfilesEditTest.java new file mode 100644 index 00000000000..cb56d80ab8b --- /dev/null +++ b/tests/src/test/java/org/sonarqube/tests/qualityProfile/QualityProfilesEditTest.java @@ -0,0 +1,126 @@ +/* + * 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.tests.qualityProfile; + +import com.sonar.orchestrator.Orchestrator; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.sonarqube.tests.Category6Suite; +import org.sonarqube.tests.Tester; +import org.sonarqube.ws.Common; +import org.sonarqube.ws.Organizations.Organization; +import org.sonarqube.ws.QualityProfiles.CreateWsResponse; +import org.sonarqube.ws.QualityProfiles.SearchUsersResponse; +import org.sonarqube.ws.WsUsers; +import org.sonarqube.ws.client.qualityprofile.AddUserRequest; +import org.sonarqube.ws.client.qualityprofile.RemoveUserRequest; +import org.sonarqube.ws.client.qualityprofile.SearchUsersRequest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; + +public class QualityProfilesEditTest { + + @ClassRule + public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR; + + @Rule + public Tester tester = new Tester(orchestrator); + + @Test + public void search_users_allowed_to_edit_a_profile() { + Organization organization = tester.organizations().generate(); + WsUsers.CreateWsResponse.User user1 = tester.users().generateMember(organization, u -> u.setEmail("user1@email.com")); + WsUsers.CreateWsResponse.User user2 = tester.users().generateMember(organization, u -> u.setEmail("user2@email.com")); + CreateWsResponse.QualityProfile xooProfile = tester.qProfiles().createXooProfile(organization); + tester.qProfiles().service().addUser(AddUserRequest.builder() + .setOrganization(organization.getKey()) + .setQualityProfile(xooProfile.getName()) + .setLanguage(xooProfile.getLanguage()) + .setUserLogin(user1.getLogin()) + .build()); + + SearchUsersResponse users = tester.qProfiles().service().searchUsers(SearchUsersRequest.builder() + .setOrganization(organization.getKey()) + .setQualityProfile(xooProfile.getName()) + .setLanguage(xooProfile.getLanguage()) + .setSelected("all") + .build()); + + assertThat(users.getUsersList()).extracting(SearchUsersResponse.User::getLogin, SearchUsersResponse.User::getName, SearchUsersResponse.User::getAvatar, SearchUsersResponse.User::getSelected) + .containsExactlyInAnyOrder( + tuple(user1.getLogin(), user1.getName(), "3acc837f898bdaa338b7cd7a9ab6dd5b", true), + tuple(user2.getLogin(), user2.getName(), "fd6926c24d76d650a365ae350784e048", false), + tuple("admin", "Administrator", "d41d8cd98f00b204e9800998ecf8427e", false)); + assertThat(users.getPaging()).extracting(Common.Paging::getPageIndex, Common.Paging::getPageSize, Common.Paging::getTotal) + .containsExactlyInAnyOrder(1, 25, 3); + } + + @Test + public void add_and_remove_user() { + Organization organization = tester.organizations().generate(); + WsUsers.CreateWsResponse.User user1 = tester.users().generateMember(organization); + WsUsers.CreateWsResponse.User user2 = tester.users().generateMember(organization); + CreateWsResponse.QualityProfile xooProfile = tester.qProfiles().createXooProfile(organization); + + // No user added + assertThat(tester.qProfiles().service().searchUsers(SearchUsersRequest.builder() + .setOrganization(organization.getKey()) + .setQualityProfile(xooProfile.getName()) + .setLanguage(xooProfile.getLanguage()) + .setSelected("selected") + .build()).getUsersList()) + .extracting(SearchUsersResponse.User::getLogin) + .isEmpty(); + + // Add user 1 + tester.qProfiles().service().addUser(AddUserRequest.builder() + .setOrganization(organization.getKey()) + .setQualityProfile(xooProfile.getName()) + .setLanguage(xooProfile.getLanguage()) + .setUserLogin(user1.getLogin()) + .build()); + assertThat(tester.qProfiles().service().searchUsers(SearchUsersRequest.builder() + .setOrganization(organization.getKey()) + .setQualityProfile(xooProfile.getName()) + .setLanguage(xooProfile.getLanguage()) + .setSelected("selected") + .build()).getUsersList()) + .extracting(SearchUsersResponse.User::getLogin) + .containsExactlyInAnyOrder(user1.getLogin()); + + // Remove user 1 + tester.qProfiles().service().removeUser(RemoveUserRequest.builder() + .setOrganization(organization.getKey()) + .setQualityProfile(xooProfile.getName()) + .setLanguage(xooProfile.getLanguage()) + .setUserLogin(user1.getLogin()) + .build()); + assertThat(tester.qProfiles().service().searchUsers(SearchUsersRequest.builder() + .setOrganization(organization.getKey()) + .setQualityProfile(xooProfile.getName()) + .setLanguage(xooProfile.getLanguage()) + .setSelected("selected") + .build()).getUsersList()) + .extracting(SearchUsersResponse.User::getLogin) + .isEmpty(); + } +} -- 2.39.5