]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-1330 Search for user allowed to edit single quality profile
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Fri, 22 Sep 2017 15:34:34 +0000 (17:34 +0200)
committerStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 2 Oct 2017 15:18:15 +0000 (17:18 +0200)
17 files changed:
server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditUsersDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditUsersMapper.java
server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/SearchUsersQuery.java [new file with mode: 0644]
server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/UserMembershipDto.java [new file with mode: 0644]
server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QProfileEditUsersMapper.xml
server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QProfileEditUsersDaoTest.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/FakeAvatarResolver.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/SearchUsersAction.java
server/sonar-server/src/main/resources/org/sonar/server/qualityprofile/ws/search_users-example.json
server/sonar-server/src/test/java/org/sonar/server/issue/ws/AvatarResolverImplTest.java
server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/SearchUsersActionTest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/qualityprofile/QualityProfilesService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/qualityprofile/SearchUsersRequest.java [new file with mode: 0644]
sonar-ws/src/main/protobuf/ws-qualityprofiles.proto
sonar-ws/src/test/java/org/sonarqube/ws/client/qualityprofile/QualityProfilesServiceTest.java
tests/src/test/java/org/sonarqube/tests/Category6Suite.java
tests/src/test/java/org/sonarqube/tests/qualityProfile/QualityProfilesEditTest.java [new file with mode: 0644]

index b173b0673321e42c427af02d098ff4d89ffeaad4..2cab9a929aa3ba6a114a25f096e91e014dd5985c 100644 (file)
  */
 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<UserMembershipDto> 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());
   }
index 601b03791f8060723aa0df9dff6eb254463556fe..70694c3307f202289f5fd765c63aa924e1f9886d 100644 (file)
  */
 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<UserMembershipDto> 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 (file)
index 0000000..cdb6077
--- /dev/null
@@ -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<String> 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 (file)
index 0000000..7c30145
--- /dev/null
@@ -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;
+  }
+
+}
index a56278ea9d8695bf6b4e73c3a7e6e6f0d9bf27e5..8bf84485bc4ee73322273667a52ef3d93f925c68 100644 (file)
     and qeu.qprofile_uuid = #{qProfileUuid, jdbcType=VARCHAR}
   </select>
 
+  <select id="countByQuery" resultType="int">
+    select count(u.id)
+    <include refid="sqlSelectByQuery" />
+  </select>
+
+  <select id="selectByQuery" parameterType="map" resultType="org.sonar.db.qualityprofile.UserMembershipDto">
+    SELECT u.id as userId, u.name as name, qeu.uuid as uuid
+    <include refid="sqlSelectByQuery"/>
+    ORDER BY u.name ASC
+    LIMIT #{pagination.pageSize,jdbcType=INTEGER}
+    OFFSET #{pagination.offset,jdbcType=INTEGER}
+  </select>
+
+  <select id="selectByQuery" parameterType="map" resultType="org.sonar.db.qualityprofile.UserMembershipDto" databaseId="mssql">
+    select * from (
+    select row_number() over(order by u.name asc) as number,
+      u.id as userId, u.name as name, qeu.uuid as uuid
+      <include refid="sqlSelectByQuery" />
+    ) as query
+    where
+    query.number between #{pagination.startRowNumber,jdbcType=INTEGER} and #{pagination.endRowNumber,jdbcType=INTEGER}
+    order by query.name asc
+  </select>
+
+  <select id="selectByQuery" parameterType="map" resultType="org.sonar.db.qualityprofile.UserMembershipDto" databaseId="oracle">
+    select * from (
+      select rownum as rn, t.* from (
+        select u.id as userId, u.name as name, qeu.uuid as uuid
+        <include refid="sqlSelectByQuery"/>
+        order by u.name ASC
+      ) t
+    ) t
+    where
+    t.rn between #{pagination.startRowNumber,jdbcType=INTEGER} and #{pagination.endRowNumber,jdbcType=INTEGER}
+  </select>
+
+  <sql id="sqlSelectByQuery">
+    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}
+    <where>
+      <choose>
+        <when test="query.getMembership() == 'IN'">
+          AND qeu.uuid IS NOT NULL
+        </when>
+        <when test="query.getMembership() == 'OUT'">
+          AND qeu.uuid IS NULL
+        </when>
+      </choose>
+      <if test="query.getQuery() != null">
+        AND (
+        lower(u.name) like #{query.querySqlLowercase} ESCAPE '/'
+        or u.login like #{query.querySql} ESCAPE '/')
+      </if>
+      AND u.active=${_true}
+    </where>
+  </sql>
+
   <insert id="insert" useGeneratedKeys="false" parameterType="map">
     insert into qprofile_edit_users(
     uuid,
     user_id,
     qprofile_uuid,
-    created_at,
+    created_at
     ) values (
     #{dto.uuid, jdbcType=VARCHAR},
     #{dto.userId, jdbcType=INTEGER},
index 5f0992e9a8e9c9801923ae791c076a4fe2d60499..e0a90e7cbb45ceca94cc88c6fe6cddc0ef45d29d 100644 (file)
@@ -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 (file)
index 0000000..c830925
--- /dev/null
@@ -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";
+  }
+
+}
index 4accbeb40915eca1a8965db9fd9917a1aa450c87..44945638c0f40d46b74ff2b78936664199914964 100644 (file)
  */
 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<SelectionMode, String> 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<UserMembershipDto> usersMembership = dbClient.qProfileEditUsersDao().selectByQuery(dbSession, query,
+        forPage(wsRequest.getPage()).andSize(wsRequest.getPageSize()));
+      Map<Integer, UserDto> 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();
+  }
+
 }
index 2eb7a6368302890225c1dd8ab5cb85296637184c..b6f08a35742d57cecb482113f829544913b2c74b 100644 (file)
@@ -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
-  }
+  ]
 }
index c2b932c50e8021330ec18b9b32d9a45653c22cf8..bd3e5c2bb9f749613168c0c05a699c14a8f8ee13 100644 (file)
@@ -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")));
index 7b28d74bdf68d4b1d2011a6c145c94a4a663aced..bcaf668c04627a47476bd8e9842181118ce346bf 100644 (file)
@@ -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();
   }
 }
index 340fd30d4a577533af57313a3ffdf7e227e98fa9..bc95c27b7b6a7cc090de955ea91dd3775687186d 100644 (file)
@@ -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 (file)
index 0000000..247efbd
--- /dev/null
@@ -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);
+    }
+  }
+}
index a02f33fe395c5e28f46eda66c69ed2ec877630ae..2300e8baa77e3d5ae7006c638aa88ab3e3927dd9 100644 (file)
@@ -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;
+  }
+}
index 63c4fcc8e8d145176f361fd6394b16e5cb084f94..655686b3e074a4917b19ecd8763e2020ad82f52a 100644 (file)
@@ -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();
+  }
 }
index 660ff241cd1aecf0a31a5c72856b635d22107a36..8fc6f4f49b5b4fef7480ac9e80fc02dfb9ed13c5 100644 (file)
@@ -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 (file)
index 0000000..cb56d80
--- /dev/null
@@ -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();
+  }
+}