diff options
author | Belen Pruvost <belen.pruvost@sonarsource.com> | 2022-07-14 10:57:26 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-07-25 20:03:57 +0000 |
commit | 34e8dfb83433a7a2d016131c9f1be7373bffb7be (patch) | |
tree | 547fe118621e11536f32a7c6d74f68220844b7e5 /server | |
parent | d66a7c69cbef6ee9647ac0233d915b2863c06338 (diff) | |
download | sonarqube-34e8dfb83433a7a2d016131c9f1be7373bffb7be.tar.gz sonarqube-34e8dfb83433a7a2d016131c9f1be7373bffb7be.zip |
SONAR-16371 - Implement pull_taint endpoint
Diffstat (limited to 'server')
22 files changed, 1509 insertions, 221 deletions
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java index 5d553b776a5..c95ee71c47c 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.Set; +import javax.annotation.Nullable; import org.sonar.db.Dao; import org.sonar.db.DbSession; import org.sonar.db.Pagination; @@ -30,6 +31,7 @@ import org.sonar.db.RowNotFoundException; import org.sonar.db.WildcardPosition; import org.sonar.db.component.ComponentDto; +import static java.util.Collections.emptyList; import static org.sonar.db.DaoUtils.buildLikeValue; import static org.sonar.db.DatabaseUtils.executeLargeInputs; @@ -59,11 +61,27 @@ public class IssueDao implements Dao { } public Set<String> selectIssueKeysByComponentUuid(DbSession session, String componentUuid) { - return mapper(session).selectIssueKeysByComponentUuid(componentUuid); + return selectIssueKeysByComponentUuid(session, componentUuid, emptyList(), emptyList(), emptyList(), + null, false); + } + + public Set<String> selectIssueKeysByComponentUuid(DbSession session, String componentUuid, + List<String> includingRepositories, List<String> excludingRepositories, + List<String> languages, @Nullable Boolean resolvedOnly, boolean openIssuesOnly) { + return mapper(session).selectIssueKeysByComponentUuid(componentUuid, includingRepositories, excludingRepositories, + languages, resolvedOnly, openIssuesOnly); } public Set<String> selectIssueKeysByComponentUuidAndChangedSinceDate(DbSession session, String componentUuid, long changedSince) { - return mapper(session).selectIssueKeysByComponentUuidAndChangedSinceDate(componentUuid, changedSince); + return selectIssueKeysByComponentUuidAndChangedSinceDate(session, componentUuid, changedSince, emptyList(), emptyList(), + emptyList(), null); + } + + public Set<String> selectIssueKeysByComponentUuidAndChangedSinceDate(DbSession session, String componentUuid, long changedSince, + List<String> includingRepositories, List<String> excludingRepositories, + List<String> languages, @Nullable Boolean resolvedOnly) { + return mapper(session).selectIssueKeysByComponentUuidAndChangedSinceDate(componentUuid, changedSince, + includingRepositories, excludingRepositories, languages, resolvedOnly); } public List<IssueDto> selectByComponentUuidPaginated(DbSession session, String componentUuid, int page) { diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java index 7611cff2f3c..1e063a82b15 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java @@ -87,6 +87,8 @@ public final class IssueDto implements Serializable { */ private Long selectedAt; + private Integer priority; + // joins private String ruleKey; private String ruleRepo; @@ -581,6 +583,16 @@ public final class IssueDto implements Serializable { return this; } + @CheckForNull + public Integer getPriority() { + return priority; + } + + public IssueDto setPriority(@Nullable Integer priority) { + this.priority = priority; + return this; + } + /** * Should only be used to persist in E/S * <p/> diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java index 3862db81734..3baf1db3549 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java @@ -22,6 +22,7 @@ package org.sonar.db.issue; import java.util.Collection; import java.util.List; import java.util.Set; +import javax.annotation.Nullable; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.session.ResultHandler; import org.sonar.db.Pagination; @@ -37,12 +38,20 @@ public interface IssueMapper { List<IssueDto> selectByKeys(List<String> keys); - Set<String> selectIssueKeysByComponentUuid(@Param("componentUuid") String componentUuid); + Set<String> selectIssueKeysByComponentUuid(@Param("componentUuid") String componentUuid, + @Param("includingRepositories") List<String> includingRepositories, + @Param("excludingRepositories") List<String> excludingRepositories, + @Param("languages") List<String> languages, @Param("resolvedOnly") @Nullable Boolean resolvedOnly, + @Param("openIssuesOnly") boolean openIssuesOnly); - Set<String> selectIssueKeysByComponentUuidAndChangedSinceDate(@Param("componentUuid") String componentUuid, @Param("changedSince") long changedSince); + Set<String> selectIssueKeysByComponentUuidAndChangedSinceDate(@Param("componentUuid") String componentUuid, + @Param("changedSince") long changedSince, + @Param("includingRepositories") List<String> includingRepositories, + @Param("excludingRepositories") List<String> excludingRepositories, + @Param("languages") List<String> languages, @Param("resolvedOnly") @Nullable Boolean resolvedOnly); List<IssueDto> selectByComponentUuidPaginated(@Param("componentUuid") String componentUuid, - @Param("pagination") Pagination pagination); + @Param("pagination") Pagination pagination); List<IssueDto> selectByKeysIfNotUpdatedAt(@Param("keys") List<String> keys, @Param("updatedAt") long updatedAt); diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueQueryParams.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueQueryParams.java index 0ce8683422b..1a5199244de 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueQueryParams.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueQueryParams.java @@ -19,23 +19,28 @@ */ package org.sonar.db.issue; +import java.util.ArrayList; import java.util.List; import javax.annotation.CheckForNull; import javax.annotation.Nullable; +import static java.util.Objects.requireNonNullElse; + public class IssueQueryParams { private final String branchUuid; private final List<String> languages; - private final List<String> ruleRepositories; private final boolean resolvedOnly; private final Long changedSince; + private final List<String> ruleRepositories; + private final List<String> excludingRuleRepositories; - public IssueQueryParams(String branchUuid, @Nullable List<String> languages, - @Nullable List<String> ruleRepositories, boolean resolvedOnly, @Nullable Long changedSince) { + public IssueQueryParams(String branchUuid, @Nullable List<String> languages, @Nullable List<String> ruleRepositories, + @Nullable List<String> excludingRuleRepositories, boolean resolvedOnly, @Nullable Long changedSince) { this.branchUuid = branchUuid; - this.languages = languages; - this.ruleRepositories = ruleRepositories; + this.languages = requireNonNullElse(languages, new ArrayList<>()); + this.ruleRepositories = requireNonNullElse(ruleRepositories, new ArrayList<>()); + this.excludingRuleRepositories = requireNonNullElse(excludingRuleRepositories, new ArrayList<>()); this.resolvedOnly = resolvedOnly; this.changedSince = changedSince; } @@ -44,16 +49,18 @@ public class IssueQueryParams { return branchUuid; } - @CheckForNull public List<String> getLanguages() { return languages; } - @CheckForNull public List<String> getRuleRepositories() { return ruleRepositories; } + public List<String> getExcludingRuleRepositories() { + return excludingRuleRepositories; + } + public boolean isResolvedOnly() { return resolvedOnly; } diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml index d78ed8ec0b1..2abc40a7e3f 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml @@ -473,17 +473,74 @@ select i.kee from issues i - where + <if test="includingRepositories != null || excludingRepositories != null || languages != null"> + inner join rules r on i.rule_uuid = r.uuid + </if> + where i.project_uuid=#{componentUuid,jdbcType=VARCHAR} + <choose> + <when test="openIssuesOnly == true"> + AND i.issue_type != 4 + AND i.status != 'CLOSED' + </when> + <when test="resolvedOnly == true"> + AND i.issue_type != 4 + AND i.status = 'RESOLVED' + </when> + </choose> + <if test="includingRepositories != null and includingRepositories.size() > 0"> + AND r.plugin_name IN + <foreach item="ruleRepository" index="index" collection="includingRepositories" open="(" separator="," close=")"> + #{ruleRepository} + </foreach> + </if> + <if test="excludingRepositories != null and excludingRepositories.size() > 0"> + AND r.plugin_name NOT IN + <foreach item="ruleRepository" index="index" collection="excludingRepositories" open="(" separator="," close=")"> + #{ruleRepository} + </foreach> + </if> + <if test="languages != null and languages.size() > 0"> + AND r.language IN + <foreach item="language" index="index" collection="languages" open="(" separator="," close=")"> + #{language} + </foreach> + </if> </select> <select id="selectIssueKeysByComponentUuidAndChangedSinceDate" parameterType="map" resultType="string"> select i.kee from issues i + <if test="includingRepositories != null || excludingRepositories != null || languages != null"> + inner join rules r on i.rule_uuid = r.uuid + </if> where i.project_uuid=#{componentUuid,jdbcType=VARCHAR} AND i.issue_update_date >= #{changedSince,jdbcType=BIGINT} + AND i.status != 'CLOSED' + AND i.issue_type != 4 + <if test="resolvedOnly == true"> + AND i.status = 'RESOLVED' + </if> + <if test="includingRepositories != null and includingRepositories.size() > 0"> + AND r.plugin_name IN + <foreach item="ruleRepository" index="index" collection="includingRepositories" open="(" separator="," close=")"> + #{ruleRepository} + </foreach> + </if> + <if test="excludingRepositories != null and excludingRepositories.size() > 0"> + AND r.plugin_name NOT IN + <foreach item="ruleRepository" index="index" collection="excludingRepositories" open="(" separator="," close=")"> + #{ruleRepository} + </foreach> + </if> + <if test="languages != null and languages.size() > 0"> + AND r.language IN + <foreach item="language" index="index" collection="languages" open="(" separator="," close=")"> + #{language} + </foreach> + </if> </select> <select id="selectByComponentUuidPaginated" parameterType="map" resultType="Issue"> @@ -551,6 +608,7 @@ result.ruleUuid as ruleUuid, result.createdAt as createdAt, result.status as status, + result.priority, result.ruleType as ruleType, result.ruleRepo as ruleRepo, result.ruleKey as ruleKey, @@ -560,7 +618,8 @@ result.type as type, result.locations as locations, result.component_uuid, - c.path as filePath + c.path as filePath, + result.assigneeUuid </sql> <sql id="selectByBranchColumnsOuterQuery"> @@ -568,6 +627,7 @@ t.ruleUuid as ruleUuid, t.createdAt as createdAt, t.status as status, + t.priority, t.ruleType as ruleType, t.ruleRepo as ruleRepo, t.ruleKey as ruleKey, @@ -576,7 +636,8 @@ t.manualSeverity as manualSeverity, t.type as type, t.locations as locations, - t.component_uuid + t.component_uuid, + t.assigneeUuid </sql> <sql id="selectByBranchColumns"> @@ -584,6 +645,7 @@ i.rule_uuid as ruleUuid, i.created_at as createdAt, i.status as status, + r.priority, r.rule_type as ruleType, r.plugin_name as ruleRepo, r.plugin_rule_key as ruleKey, @@ -592,7 +654,8 @@ i.manual_severity as manualSeverity, i.issue_type as type, i.locations as locations, - i.component_uuid as component_uuid + i.component_uuid as component_uuid, + i.assignee as assigneeUuid </sql> <select id="selectByBranch" parameterType="map" resultType="Issue"> @@ -604,28 +667,28 @@ inner join components p on p.uuid=i.component_uuid where i.project_uuid = #{queryParams.branchUuid} - <if test="queryParams.changedSince != null"> - AND i.issue_update_date >= #{queryParams.changedSince,jdbcType=BIGINT} - </if> - <if test="queryParams.resolvedOnly == true"> - AND i.status = 'RESOLVED' - </if> AND i.status <> 'CLOSED' AND i.issue_type <> 4 - <if test="queryParams.ruleRepositories != null"> - AND r.plugin_name IN - <foreach item="ruleRepository" index="index" collection="queryParams.ruleRepositories" open="(" separator="," close=")"> - #{ruleRepository} - </foreach> - </if> - <if test="queryParams.languages != null"> - AND r.language IN - <foreach item="language" index="index" collection="queryParams.languages" open="(" separator="," close=")"> - #{language} - </foreach> - </if> - order by i.kee ASC - limit #{pagination.pageSize,jdbcType=INTEGER} offset #{pagination.offset,jdbcType=INTEGER} + <if test="queryParams.changedSince != null"> + AND i.issue_update_date >= #{queryParams.changedSince,jdbcType=BIGINT} + </if> + <if test="queryParams.resolvedOnly == true"> + AND i.status = 'RESOLVED' + </if> + <if test="queryParams.ruleRepositories.size() > 0"> + AND r.plugin_name IN + <foreach item="ruleRepository" index="index" collection="queryParams.ruleRepositories" open="(" separator="," close=")"> + #{ruleRepository} + </foreach> + </if> + <if test="queryParams.languages.size() > 0"> + AND r.language IN + <foreach item="language" index="index" collection="queryParams.languages" open="(" separator="," close=")"> + #{language} + </foreach> + </if> + order by i.kee ASC + limit #{pagination.pageSize,jdbcType=INTEGER} offset #{pagination.offset,jdbcType=INTEGER} </select> <select id="selectByBranch" parameterType="map" resultType="Issue" databaseId="mssql"> @@ -642,21 +705,21 @@ inner join rules r on r.uuid = i.rule_uuid where i.project_uuid = #{queryParams.branchUuid} - <if test="queryParams.changedSince != null"> - AND i.issue_update_date >= #{queryParams.changedSince,jdbcType=BIGINT} - </if> - <if test="queryParams.resolvedOnly == true"> - AND i.status = 'RESOLVED' - </if> AND i.status <> 'CLOSED' - AND i.issue_type <> 4 - <if test="queryParams.ruleRepositories != null"> + AND i.issue_type <> 4 + <if test="queryParams.changedSince != null"> + AND i.issue_update_date >= #{queryParams.changedSince,jdbcType=BIGINT} + </if> + <if test="queryParams.resolvedOnly == true"> + AND i.status = 'RESOLVED' + </if> + <if test="queryParams.ruleRepositories.size() > 0"> AND r.plugin_name IN <foreach item="ruleRepository" index="index" collection="queryParams.ruleRepositories" open="(" separator="," close=")"> #{ruleRepository} </foreach> </if> - <if test="queryParams.languages != null"> + <if test="queryParams.languages.size() > 0"> AND r.language IN <foreach item="language" index="index" collection="queryParams.languages" open="(" separator="," close=")"> #{language} @@ -679,21 +742,21 @@ inner join rules r on r.uuid = i.rule_uuid where i.project_uuid = #{queryParams.branchUuid} - <if test="queryParams.changedSince != null"> - AND i.issue_update_date >= #{queryParams.changedSince,jdbcType=BIGINT} - </if> - <if test="queryParams.resolvedOnly == true"> - AND i.status = 'RESOLVED' - </if> AND i.status <> 'CLOSED' - AND i.issue_type <> 4 - <if test="queryParams.ruleRepositories != null"> + AND i.issue_type <> 4 + <if test="queryParams.changedSince != null"> + AND i.issue_update_date >= #{queryParams.changedSince,jdbcType=BIGINT} + </if> + <if test="queryParams.resolvedOnly == true"> + AND i.status = 'RESOLVED' + </if> + <if test="queryParams.ruleRepositories.size() > 0"> AND r.plugin_name IN <foreach item="ruleRepository" index="index" collection="queryParams.ruleRepositories" open="(" separator="," close=")"> #{ruleRepository} </foreach> </if> - <if test="queryParams.languages != null"> + <if test="queryParams.languages.size() > 0"> AND r.language IN <foreach item="language" index="index" collection="queryParams.languages" open="(" separator="," close=")"> #{language} @@ -715,19 +778,25 @@ where i.project_uuid = #{queryParams.branchUuid} AND issue_update_date >= #{queryParams.changedSince} - <if test="queryParams.ruleRepositories != null"> + AND i.status = 'CLOSED' + <if test="queryParams.ruleRepositories.size() > 0"> AND r.plugin_name IN <foreach item="ruleRepository" index="index" collection="queryParams.ruleRepositories" open="(" separator="," close=")"> #{ruleRepository} </foreach> </if> - <if test="queryParams.languages != null"> + <if test="queryParams.excludingRuleRepositories.size() > 0"> + AND r.plugin_name NOT IN + <foreach item="ruleRepository" index="index" collection="queryParams.excludingRuleRepositories" open="(" separator="," close=")"> + #{ruleRepository} + </foreach> + </if> + <if test="queryParams.languages.size() > 0"> AND r.language IN <foreach item="language" index="index" collection="queryParams.languages" open="(" separator="," close=")"> #{language} </foreach> </if> - AND i.status = 'CLOSED' </select> </mapper> diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java index c74730f99a4..fa29f8053a2 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java @@ -41,6 +41,7 @@ import org.sonar.db.rule.RuleDto; import org.sonar.db.rule.RuleTesting; import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; import static org.apache.commons.lang.math.RandomUtils.nextInt; import static org.assertj.core.api.Assertions.assertThat; @@ -152,6 +153,43 @@ public class IssueDaoTest { } @Test + public void selectIssueKeysByComponentUuidFiltersAccordingly() { + // contains I1 and I2 + prepareTables(); + + // adds I3 + underTest.insert(db.getSession(), newIssueDto("I3") + .setMessage("the message") + .setRuleUuid(RULE.getUuid()) + .setComponentUuid(FILE_UUID) + .setStatus("OPEN") + .setProjectUuid(PROJECT_UUID)); + + // Filter by including repositories + Set<String> issues = underTest.selectIssueKeysByComponentUuid(db.getSession(), PROJECT_UUID, List.of("xoo"), + emptyList(), emptyList(), null, false); + // results are not ordered, so do not use "containsExactly" + assertThat(issues).containsOnly("I1", "I2", "I3"); + + // Filter by excluding repositories + issues = underTest.selectIssueKeysByComponentUuid(db.getSession(), PROJECT_UUID, emptyList(), List.of("xoo"), + emptyList(), null, false); + assertThat(issues).isEmpty(); + + // Filter by language + issues = underTest.selectIssueKeysByComponentUuid(db.getSession(), PROJECT_UUID, emptyList(), emptyList(), List.of("xoo"), null, false); + assertThat(issues).containsOnly("I1", "I2", "I3"); + + // Filter by resolved only + issues = underTest.selectIssueKeysByComponentUuid(db.getSession(), PROJECT_UUID, emptyList(), emptyList(), emptyList(), true, false); + assertThat(issues).containsOnly("I1"); + + // Filter by non-closed issues only + issues = underTest.selectIssueKeysByComponentUuid(db.getSession(), PROJECT_UUID, emptyList(), emptyList(), emptyList(), null, true); + assertThat(issues).containsOnly("I1", "I3"); + } + + @Test public void selectIssueKeysByComponentUuidAndChangedSince() { long t1 = 1_340_000_000_000L; long t2 = 1_400_000_000_000L; @@ -163,9 +201,41 @@ public class IssueDaoTest { Set<String> issues = underTest.selectIssueKeysByComponentUuidAndChangedSinceDate(db.getSession(), PROJECT_UUID, t2); // results are not ordered, so do not use "containsExactly" - assertThat(issues).containsOnly("I1", "I2"); + assertThat(issues).containsOnly("I1"); + } + + @Test + public void selectIssueKeysByComponentUuidAndChangedSinceFiltersAccordingly() { + long t1 = 1_340_000_000_000L; + long t2 = 1_400_000_000_000L; + // contains I1 and I2 + prepareTables(); + // Insert I3, I4, where t1 < t2 + IntStream.range(3, 5).forEach(i -> underTest.insert(db.getSession(), newIssueDto("I" + i).setUpdatedAt(t1))); + + // Filter by including repositories + Set<String> issues = underTest.selectIssueKeysByComponentUuidAndChangedSinceDate(db.getSession(), PROJECT_UUID, t2, List.of("xoo"), + emptyList(), emptyList(), null); + // results are not ordered, so do not use "containsExactly" + assertThat(issues).containsOnly("I1"); + + // Filter by excluding repositories + issues = underTest.selectIssueKeysByComponentUuidAndChangedSinceDate(db.getSession(), PROJECT_UUID, t2, + emptyList(), List.of("xoo"), emptyList(), null); + assertThat(issues).isEmpty(); + + // Filter by language + issues = underTest.selectIssueKeysByComponentUuidAndChangedSinceDate(db.getSession(), PROJECT_UUID, t2, emptyList(), + emptyList(), List.of("xoo"), null); + assertThat(issues).containsOnly("I1"); + + // Filter by resolved only + issues = underTest.selectIssueKeysByComponentUuidAndChangedSinceDate(db.getSession(), PROJECT_UUID, t2, emptyList(), + emptyList(), emptyList(), true); + assertThat(issues).containsOnly("I1"); } + @Test public void selectByBranch() { long updatedAt = 1_340_000_000_000L; @@ -246,11 +316,11 @@ public class IssueDaoTest { assertThat(underTest.selectNonClosedByComponentUuidExcludingExternalsAndSecurityHotspots(db.getSession(), file.uuid())) .extracting(IssueDto::getKey) - .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[] {openIssue1OnFile, openIssue2OnFile}).map(IssueDto::getKey).toArray(String[]::new)); + .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[]{openIssue1OnFile, openIssue2OnFile}).map(IssueDto::getKey).toArray(String[]::new)); assertThat(underTest.selectNonClosedByComponentUuidExcludingExternalsAndSecurityHotspots(db.getSession(), project.uuid())) .extracting(IssueDto::getKey) - .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[] {openIssueOnProject}).map(IssueDto::getKey).toArray(String[]::new)); + .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[]{openIssueOnProject}).map(IssueDto::getKey).toArray(String[]::new)); assertThat(underTest.selectNonClosedByComponentUuidExcludingExternalsAndSecurityHotspots(db.getSession(), "does_not_exist")).isEmpty(); } @@ -278,11 +348,11 @@ public class IssueDaoTest { assertThat(underTest.selectNonClosedByModuleOrProjectExcludingExternalsAndSecurityHotspots(db.getSession(), project)) .extracting(IssueDto::getKey) .containsExactlyInAnyOrder( - Arrays.stream(new IssueDto[] {openIssue1OnFile, openIssue2OnFile, openIssueOnModule, openIssueOnProject}).map(IssueDto::getKey).toArray(String[]::new)); + Arrays.stream(new IssueDto[]{openIssue1OnFile, openIssue2OnFile, openIssueOnModule, openIssueOnProject}).map(IssueDto::getKey).toArray(String[]::new)); assertThat(underTest.selectNonClosedByModuleOrProjectExcludingExternalsAndSecurityHotspots(db.getSession(), module)) .extracting(IssueDto::getKey) - .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[] {openIssue1OnFile, openIssue2OnFile, openIssueOnModule}).map(IssueDto::getKey).toArray(String[]::new)); + .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[]{openIssue1OnFile, openIssue2OnFile, openIssueOnModule}).map(IssueDto::getKey).toArray(String[]::new)); ComponentDto notPersisted = ComponentTesting.newPrivateProjectDto(); assertThat(underTest.selectNonClosedByModuleOrProjectExcludingExternalsAndSecurityHotspots(db.getSession(), notPersisted)).isEmpty(); @@ -787,6 +857,7 @@ public class IssueDaoTest { underTest.insert(db.getSession(), newIssueDto(ISSUE_KEY2) .setRuleUuid(RULE.getUuid()) .setComponentUuid(FILE_UUID) + .setStatus("CLOSED") .setProjectUuid(PROJECT_UUID)); db.getSession().commit(); } @@ -800,6 +871,6 @@ public class IssueDaoTest { } private static IssueQueryParams buildSelectByBranchQuery(ComponentDto branch, String language, boolean resolvedOnly, Long changedSince) { - return new IssueQueryParams(branch.uuid(), List.of(language), List.of(language), resolvedOnly, changedSince); + return new IssueQueryParams(branch.uuid(), List.of(language), List.of(), List.of(), resolvedOnly, changedSince); } } diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueQueryParamsTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueQueryParamsTest.java index b23a2bbcc75..b68b4cd11e2 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueQueryParamsTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueQueryParamsTest.java @@ -25,7 +25,6 @@ import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; public class IssueQueryParamsTest { - private final List<String> languages = List.of("java"); private final List<String> ruleRepositories = List.of("js-security", "java"); @Test @@ -34,11 +33,12 @@ public class IssueQueryParamsTest { long changedSince = 1_000_000L; String branchUuid = "master-branch-uuid"; - IssueQueryParams queryParameters = new IssueQueryParams(branchUuid, languages, ruleRepositories, resolvedOnly, changedSince); + IssueQueryParams queryParameters = new IssueQueryParams(branchUuid, null, ruleRepositories, null, resolvedOnly, changedSince); assertThat(queryParameters.getBranchUuid()).isEqualTo(branchUuid); - assertThat(queryParameters.getLanguages()).isEqualTo(languages); + assertThat(queryParameters.getLanguages()).isNotNull().isEmpty(); assertThat(queryParameters.getRuleRepositories()).isEqualTo(ruleRepositories); + assertThat(queryParameters.getExcludingRuleRepositories()).isNotNull().isEmpty(); assertThat(queryParameters.isResolvedOnly()).isFalse(); assertThat(queryParameters.getChangedSince()).isEqualTo(changedSince); diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/TaintChecker.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/TaintChecker.java index 0abd66b3756..ed591a9127d 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/TaintChecker.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/TaintChecker.java @@ -22,7 +22,6 @@ package org.sonar.server.issue; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; @@ -30,7 +29,7 @@ import org.sonar.db.issue.IssueDto; public class TaintChecker { - private static final Set<String> TAINT_REPOSITORIES = Set.of("roslyn.sonaranalyzer.security.cs", "javasecurity", "jssecurity", "tssecurity", "phpsecurity", "pythonsecurity"); + private static final List<String> TAINT_REPOSITORIES = List.of("roslyn.sonaranalyzer.security.cs", "javasecurity", "jssecurity", "tssecurity", "phpsecurity", "pythonsecurity"); private TaintChecker() { throw new IllegalStateException("Utility class, cannot be instantiated."); @@ -65,4 +64,8 @@ public class TaintChecker { return issueDto -> !TAINT_REPOSITORIES.contains(issueDto.getRuleRepo()); } + public static List<String> getTaintRepositories() { + return TAINT_REPOSITORIES; + } + } diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/TaintCheckerTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/TaintCheckerTest.java index e98b2ba45ba..7da4d7a26ee 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/TaintCheckerTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/TaintCheckerTest.java @@ -78,6 +78,14 @@ public class TaintCheckerTest { assertThat(issuesByTaintStatus.get(false).get(2).getKey()).isEqualTo("standardIssue3"); } + @Test + public void test_getTaintRepositories() { + assertThat(TaintChecker.getTaintRepositories()) + .hasSize(6) + .containsExactlyInAnyOrder("roslyn.sonaranalyzer.security.cs", "javasecurity", "jssecurity", + "tssecurity", "phpsecurity", "pythonsecurity"); + } + private List<IssueDto> getIssues() { List<IssueDto> issues = new ArrayList<>(); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BasePullAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BasePullAction.java new file mode 100644 index 00000000000..7c8b5ce2b35 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BasePullAction.java @@ -0,0 +1,183 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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 java.io.IOException; +import java.io.OutputStream; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import javax.annotation.Nullable; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.utils.System2; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.BranchDto; +import org.sonar.db.issue.IssueDto; +import org.sonar.db.issue.IssueQueryParams; +import org.sonar.db.project.ProjectDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.issue.ws.pull.ProtobufObjectGenerator; +import org.sonar.server.issue.ws.pull.PullActionIssuesRetriever; +import org.sonar.server.issue.ws.pull.PullActionResponseWriter; +import org.sonar.server.user.UserSession; + +import static java.lang.String.format; +import static java.util.Collections.emptyList; +import static org.sonar.api.web.UserRole.USER; + +public abstract class BasePullAction implements IssuesWsAction { + + protected static final String PROJECT_KEY_PARAM = "projectKey"; + protected static final String BRANCH_NAME_PARAM = "branchName"; + protected static final String LANGUAGES_PARAM = "languages"; + protected static final String RULE_REPOSITORIES_PARAM = "ruleRepositories"; + protected static final String RESOLVED_ONLY_PARAM = "resolvedOnly"; + protected static final String CHANGED_SINCE_PARAM = "changedSince"; + protected final String actionName; + protected final String issueType; + protected final String repositoryExample; + protected final String sinceVersion; + protected final String resourceExample; + + private final ComponentFinder componentFinder; + private final DbClient dbClient; + private final UserSession userSession; + private final PullActionResponseWriter pullActionResponseWriter; + + protected BasePullAction(System2 system2, ComponentFinder componentFinder, DbClient dbClient, UserSession userSession, + ProtobufObjectGenerator protobufObjectGenerator, String actionName, String issueType, + String repositoryExample, String sinceVersion, String resourceExample) { + this.componentFinder = componentFinder; + this.dbClient = dbClient; + this.userSession = userSession; + this.pullActionResponseWriter = new PullActionResponseWriter(system2, protobufObjectGenerator); + this.actionName = actionName; + this.issueType = issueType; + this.repositoryExample = repositoryExample; + this.sinceVersion = sinceVersion; + this.resourceExample = resourceExample; + } + + @Override + public void define(WebService.NewController controller) { + WebService.NewAction action = controller + .createAction(actionName) + .setHandler(this) + .setInternal(true) + .setResponseExample(getClass().getResource(resourceExample)) + .setDescription(format("This endpoint fetches and returns all (unless filtered by optional params) the %s for a given branch. " + + "The %s returned are not paginated, so the response size can be big.", issueType, issueType)) + .setSince(sinceVersion); + + action.createParam(PROJECT_KEY_PARAM) + .setRequired(true) + .setDescription(format("Project key for which %s are fetched.", issueType)) + .setExampleValue("sonarqube"); + + action.createParam(BRANCH_NAME_PARAM) + .setRequired(true) + .setDescription(format("Branch name for which %s are fetched.", issueType)) + .setExampleValue("develop"); + + action.createParam(LANGUAGES_PARAM) + .setDescription(format("Comma separated list of languages. If not present all %s regardless of their language are returned.", issueType)) + .setExampleValue("java,cobol"); + + action.createParam(CHANGED_SINCE_PARAM) + .setDescription(format("Timestamp. If present only %s modified after given timestamp are returned (both open and closed). " + + "If not present all non-closed %s are returned.", issueType, issueType)) + .setExampleValue(1_654_032_306_000L); + + if (issueType.equals("issues")) { + action.createParam(RULE_REPOSITORIES_PARAM) + .setDescription(format("Comma separated list of rule repositories. If not present all %s regardless of" + + " their rule repository are returned.", issueType)) + .setExampleValue(repositoryExample); + + action.createParam(RESOLVED_ONLY_PARAM) + .setDescription(format("If true only %s with resolved status are returned", issueType)) + .setExampleValue("true"); + } + } + + @Override + public void handle(Request request, Response response) throws Exception { + String projectKey = request.mandatoryParam(PROJECT_KEY_PARAM); + String branchName = request.mandatoryParam(BRANCH_NAME_PARAM); + List<String> languages = request.paramAsStrings(LANGUAGES_PARAM); + String changedSince = request.param(CHANGED_SINCE_PARAM); + Long changedSinceTimestamp = changedSince != null ? Long.parseLong(changedSince) : null; + + if (issueType.equals("issues")) { + boolean resolvedOnly = Boolean.parseBoolean(request.param(RESOLVED_ONLY_PARAM)); + + List<String> ruleRepositories = request.paramAsStrings(RULE_REPOSITORIES_PARAM); + if (ruleRepositories != null && !ruleRepositories.isEmpty()) { + validateRuleRepositories(ruleRepositories); + } + + streamResponse(projectKey, branchName, languages, ruleRepositories, resolvedOnly, changedSinceTimestamp, response.stream().output()); + } else { + streamResponse(projectKey, branchName, languages, emptyList(), false, changedSinceTimestamp, response.stream().output()); + } + } + + private void streamResponse(String projectKey, String branchName, @Nullable List<String> languages, + @Nullable List<String> ruleRepositories, boolean resolvedOnly, @Nullable Long changedSince, OutputStream outputStream) + throws IOException { + + try (DbSession dbSession = dbClient.openSession(false)) { + ProjectDto projectDto = componentFinder.getProjectByKey(dbSession, projectKey); + userSession.checkProjectPermission(USER, projectDto); + BranchDto branchDto = componentFinder.getBranchOrPullRequest(dbSession, projectDto, branchName, null); + pullActionResponseWriter.appendTimestampToResponse(outputStream); + IssueQueryParams issueQueryParams = initializeQueryParams(branchDto, languages, ruleRepositories, resolvedOnly, changedSince); + retrieveAndSendIssues(dbSession, issueQueryParams, outputStream); + } + } + + private void retrieveAndSendIssues(DbSession dbSession, IssueQueryParams queryParams, OutputStream outputStream) + throws IOException { + + var issuesRetriever = new PullActionIssuesRetriever(dbClient, queryParams); + + Set<String> issueKeysSnapshot = new HashSet<>(getIssueKeysSnapshot(queryParams)); + Consumer<List<IssueDto>> listConsumer = issueDtos -> pullActionResponseWriter.appendIssuesToResponse(issueDtos, outputStream); + issuesRetriever.processIssuesByBatch(dbSession, issueKeysSnapshot, listConsumer); + + if (queryParams.getChangedSince() != null) { + // in the "incremental mode" we need to send SonarLint also recently closed issues keys + List<String> closedIssues = issuesRetriever.retrieveClosedIssues(dbSession); + pullActionResponseWriter.appendClosedIssuesUuidsToResponse(closedIssues, outputStream); + } + } + + protected abstract void validateRuleRepositories(List<String> ruleRepositories); + + protected abstract IssueQueryParams initializeQueryParams(BranchDto branchDto, @Nullable List<String> languages, + @Nullable List<String> ruleRepositories, boolean resolvedOnly, @Nullable Long changedSince); + + protected abstract Set<String> getIssueKeysSnapshot(IssueQueryParams queryParams); + +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java index d6aa030e55b..815edbec1b8 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java @@ -32,6 +32,7 @@ import org.sonar.server.issue.workflow.FunctionExecutor; import org.sonar.server.issue.workflow.IssueWorkflow; import org.sonar.server.issue.ws.pull.PullActionProtobufObjectGenerator; import org.sonar.server.issue.ws.pull.PullActionResponseWriter; +import org.sonar.server.issue.ws.pull.PullTaintActionProtobufObjectGenerator; import org.sonar.server.qualitygate.changeevent.QGChangeEventListenersImpl; public class IssueWsModule extends Module { @@ -70,7 +71,9 @@ public class IssueWsModule extends Module { BulkChangeAction.class, QGChangeEventListenersImpl.class, PullAction.class, + PullTaintAction.class, PullActionResponseWriter.class, - PullActionProtobufObjectGenerator.class); + PullActionProtobufObjectGenerator.class, + PullTaintActionProtobufObjectGenerator.class); } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullAction.java index 24813f197e3..52aa0b61608 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullAction.java @@ -19,145 +19,69 @@ */ package org.sonar.server.issue.ws; -import java.io.IOException; -import java.io.OutputStream; -import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.function.Consumer; import javax.annotation.Nullable; -import org.sonar.api.server.ws.Request; -import org.sonar.api.server.ws.Response; -import org.sonar.api.server.ws.WebService; +import org.sonar.api.utils.System2; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.component.BranchDto; -import org.sonar.db.issue.IssueDto; import org.sonar.db.issue.IssueQueryParams; -import org.sonar.db.project.ProjectDto; import org.sonar.server.component.ComponentFinder; -import org.sonar.server.issue.ws.pull.PullActionIssuesRetriever; -import org.sonar.server.issue.ws.pull.PullActionResponseWriter; +import org.sonar.server.issue.TaintChecker; +import org.sonar.server.issue.ws.pull.PullActionProtobufObjectGenerator; import org.sonar.server.user.UserSession; import static java.util.Optional.ofNullable; -import static org.sonar.api.web.UserRole.USER; +import static org.sonarqube.ws.WsUtils.checkArgument; import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_PULL; -public class PullAction implements IssuesWsAction { - - private static final String PROJECT_KEY_PARAM = "projectKey"; - private static final String BRANCH_NAME_PARAM = "branchName"; - private static final String LANGUAGES_PARAM = "languages"; - private static final String RULE_REPOSITORIES_PARAM = "ruleRepositories"; - private static final String RESOLVED_ONLY_PARAM = "resolvedOnly"; - private static final String CHANGED_SINCE_PARAM = "changedSince"; +public class PullAction extends BasePullAction { + private static final String ISSUE_TYPE = "issues"; + private static final String REPOSITORY_EXAMPLE = "java"; + private static final String RESOURCE_EXAMPLE = "pull-example.proto"; + private static final String SINCE_VERSION = "9.5"; private final DbClient dbClient; - private final UserSession userSession; - private final PullActionResponseWriter pullActionResponseWriter; - private final ComponentFinder componentFinder; - public PullAction(DbClient dbClient, UserSession userSession, PullActionResponseWriter pullActionResponseWriter, ComponentFinder componentFinder) { + public PullAction(System2 system2, ComponentFinder componentFinder, DbClient dbClient, UserSession userSession, + PullActionProtobufObjectGenerator protobufObjectGenerator) { + super(system2, componentFinder, dbClient, userSession, protobufObjectGenerator, ACTION_PULL, + ISSUE_TYPE, REPOSITORY_EXAMPLE, SINCE_VERSION, RESOURCE_EXAMPLE); this.dbClient = dbClient; - this.userSession = userSession; - this.pullActionResponseWriter = pullActionResponseWriter; - this.componentFinder = componentFinder; } @Override - public void define(WebService.NewController controller) { - WebService.NewAction action = controller - .createAction(ACTION_PULL) - .setHandler(this) - .setInternal(true) - .setResponseExample(getClass().getResource("pull-example.proto")) - .setDescription("This endpoint fetches and returns all (unless filtered by optional params) the issues for a given branch." + - "The issues returned are not paginated, so the response size can be big.") - .setSince("9.5"); - - action.createParam(PROJECT_KEY_PARAM) - .setRequired(true) - .setDescription("Project key for which issues are fetched.") - .setExampleValue("sonarqube"); - - action.createParam(BRANCH_NAME_PARAM) - .setRequired(true) - .setDescription("Branch name for which issues are fetched.") - .setExampleValue("develop"); - - action.createParam(LANGUAGES_PARAM) - .setDescription("Comma seperated list of languages. If not present all issues regardless of their language are returned.") - .setExampleValue("java,cobol"); - - action.createParam(RULE_REPOSITORIES_PARAM) - .setDescription("Comma seperated list of rule repositories. If not present all issues regardless of" + - " their rule repository are returned.") - .setExampleValue("java"); - - action.createParam(RESOLVED_ONLY_PARAM) - .setDescription("If true only issues with resolved status are returned") - .setExampleValue("true"); - - action.createParam(CHANGED_SINCE_PARAM) - .setDescription("Timestamp. If present only issues modified after given timestamp are returned (both open and closed). " + - "If not present all non-closed issues are returned.") - .setExampleValue(1_654_032_306_000L); - } - - @Override - public void handle(Request request, Response response) throws Exception { - String projectKey = request.mandatoryParam(PROJECT_KEY_PARAM); - String branchName = request.mandatoryParam(BRANCH_NAME_PARAM); - List<String> languages = request.paramAsStrings(LANGUAGES_PARAM); - List<String> ruleRepositories = request.paramAsStrings(RULE_REPOSITORIES_PARAM); - boolean resolvedOnly = Boolean.parseBoolean(request.param(RESOLVED_ONLY_PARAM)); - String changedSince = request.param(CHANGED_SINCE_PARAM); - Long changedSinceTimestamp = changedSince != null ? Long.parseLong(changedSince) : null; - - streamResponse(projectKey, branchName, languages, ruleRepositories, resolvedOnly, changedSinceTimestamp, response.stream().output()); - } + protected Set<String> getIssueKeysSnapshot(IssueQueryParams issueQueryParams) { + try (DbSession dbSession = dbClient.openSession(false)) { + Optional<Long> changedSinceDate = ofNullable(issueQueryParams.getChangedSince()); - private void streamResponse(String projectKey, String branchName, @Nullable List<String> languages, - @Nullable List<String> ruleRepositories, boolean resolvedOnly, @Nullable Long changedSince, OutputStream outputStream) - throws IOException { + if (changedSinceDate.isPresent()) { + return dbClient.issueDao().selectIssueKeysByComponentUuidAndChangedSinceDate(dbSession, issueQueryParams.getBranchUuid(), + changedSinceDate.get(), issueQueryParams.getRuleRepositories(), TaintChecker.getTaintRepositories(), + issueQueryParams.getLanguages(), issueQueryParams.isResolvedOnly()); + } - try (DbSession dbSession = dbClient.openSession(false)) { - ProjectDto projectDto = componentFinder.getProjectByKey(dbSession, projectKey); - userSession.checkProjectPermission(USER, projectDto); - BranchDto branchDto = componentFinder.getBranchOrPullRequest(dbSession, projectDto, branchName, null); - pullActionResponseWriter.appendTimestampToResponse(outputStream); - IssueQueryParams pullActionQueryParams = new IssueQueryParams(branchDto.getUuid(), languages, ruleRepositories, resolvedOnly, changedSince); - retrieveAndSendIssues(dbSession, pullActionQueryParams, outputStream); + return dbClient.issueDao().selectIssueKeysByComponentUuid(dbSession, issueQueryParams.getBranchUuid(), + issueQueryParams.getRuleRepositories(), TaintChecker.getTaintRepositories(), + issueQueryParams.getLanguages(), issueQueryParams.isResolvedOnly(), true); } } - private void retrieveAndSendIssues(DbSession dbSession, IssueQueryParams queryParams, OutputStream outputStream) - throws IOException { - - var issuesRetriever = new PullActionIssuesRetriever(dbClient, queryParams); - - Set<String> issueKeysSnapshot = new HashSet<>(getIssueKeysSnapshot(queryParams.getBranchUuid(), queryParams.getChangedSince())); - Consumer<List<IssueDto>> listConsumer = issueDtos -> pullActionResponseWriter.appendIssuesToResponse(issueDtos, outputStream); - issuesRetriever.processIssuesByBatch(dbSession, issueKeysSnapshot, listConsumer); - - if (queryParams.getChangedSince() != null) { - // in the "incremental mode" we need to send SonarLint also recently closed issues keys - List<String> closedIssues = issuesRetriever.retrieveClosedIssues(dbSession); - pullActionResponseWriter.appendClosedIssuesUuidsToResponse(closedIssues, outputStream); - } + @Override + protected IssueQueryParams initializeQueryParams(BranchDto branchDto, @Nullable List<String> languages, + @Nullable List<String> ruleRepositories, boolean resolvedOnly, @Nullable Long changedSince) { + return new IssueQueryParams(branchDto.getUuid(), languages, ruleRepositories, TaintChecker.getTaintRepositories(), resolvedOnly, changedSince); } - private Set<String> getIssueKeysSnapshot(String componentUuid, @Nullable Long changedSince) { - try (DbSession dbSession = dbClient.openSession(false)) { - Optional<Long> changedSinceDate = ofNullable(changedSince); - if (changedSinceDate.isPresent()) { - return dbClient.issueDao().selectIssueKeysByComponentUuidAndChangedSinceDate(dbSession, componentUuid, changedSinceDate.get()); - } + @Override + protected void validateRuleRepositories(List<String> ruleRepositories) { + checkArgument(ruleRepositories + .stream() + .filter(TaintChecker.getTaintRepositories()::contains) + .count() == 0, "Incorrect rule repositories list: it should only include repositories that define Issues, and no Taint Vulnerabilities"); - return dbClient.issueDao().selectIssueKeysByComponentUuid(dbSession, componentUuid); - } } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullTaintAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullTaintAction.java new file mode 100644 index 00000000000..5c1e1f8faa7 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/PullTaintAction.java @@ -0,0 +1,82 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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 java.util.List; +import java.util.Optional; +import java.util.Set; +import javax.annotation.Nullable; +import org.sonar.api.utils.System2; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.BranchDto; +import org.sonar.db.issue.IssueQueryParams; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.issue.TaintChecker; +import org.sonar.server.issue.ws.pull.PullTaintActionProtobufObjectGenerator; +import org.sonar.server.user.UserSession; + +import static java.util.Collections.emptyList; +import static java.util.Optional.ofNullable; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_PULL_TAINT; + +public class PullTaintAction extends BasePullAction { + private static final String ISSUE_TYPE = "taint vulnerabilities"; + private static final String RESOURCE_EXAMPLE = "pull-taint-example.proto"; + private static final String SINCE_VERSION = "9.6"; + + private final DbClient dbClient; + + public PullTaintAction(System2 system2, ComponentFinder componentFinder, DbClient dbClient, UserSession userSession, + PullTaintActionProtobufObjectGenerator protobufObjectGenerator) { + super(system2, componentFinder, dbClient, userSession, protobufObjectGenerator, ACTION_PULL_TAINT, + ISSUE_TYPE, "", SINCE_VERSION, RESOURCE_EXAMPLE); + this.dbClient = dbClient; + } + + @Override + protected Set<String> getIssueKeysSnapshot(IssueQueryParams issueQueryParams) { + Optional<Long> changedSinceDate = ofNullable(issueQueryParams.getChangedSince()); + + try (DbSession dbSession = dbClient.openSession(false)) { + if (changedSinceDate.isPresent()) { + return dbClient.issueDao().selectIssueKeysByComponentUuidAndChangedSinceDate(dbSession, issueQueryParams.getBranchUuid(), + changedSinceDate.get(), issueQueryParams.getRuleRepositories(), emptyList(), + issueQueryParams.getLanguages(), false); + } + + return dbClient.issueDao().selectIssueKeysByComponentUuid(dbSession, issueQueryParams.getBranchUuid(), + issueQueryParams.getRuleRepositories(), + emptyList(), issueQueryParams.getLanguages(), + issueQueryParams.isResolvedOnly(), true); + + } + } + + @Override + protected IssueQueryParams initializeQueryParams(BranchDto branchDto, @Nullable List<String> languages, + @Nullable List<String> ruleRepositories, boolean resolvedOnly, @Nullable Long changedSince) { + return new IssueQueryParams(branchDto.getUuid(), languages, TaintChecker.getTaintRepositories(), emptyList(), resolvedOnly, changedSince); + } + + @Override + protected void validateRuleRepositories(List<String> ruleRepositories) { + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/ProtobufObjectGenerator.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/ProtobufObjectGenerator.java new file mode 100644 index 00000000000..1286e5e4aa8 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/ProtobufObjectGenerator.java @@ -0,0 +1,47 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.pull; + +import com.google.protobuf.AbstractMessageLite; +import org.sonar.db.issue.IssueDto; +import org.sonar.db.protobuf.DbIssues; +import org.sonarqube.ws.Issues; + +public interface ProtobufObjectGenerator { + AbstractMessageLite generateTimestampMessage(long timestamp); + + AbstractMessageLite generateIssueMessage(IssueDto issueDto); + + AbstractMessageLite generateClosedIssueMessage(String uuid); + + default Issues.TextRange buildTextRange(DbIssues.Locations mainLocation) { + int startLine = mainLocation.getTextRange().getStartLine(); + int endLine = mainLocation.getTextRange().getEndLine(); + int startOffset = mainLocation.getTextRange().getStartOffset(); + int endOffset = mainLocation.getTextRange().getEndOffset(); + + return Issues.TextRange.newBuilder() + .setHash(mainLocation.getChecksum()) + .setStartLine(startLine) + .setEndLine(endLine) + .setStartLineOffset(startOffset) + .setEndLineOffset(endOffset).build(); + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionProtobufObjectGenerator.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionProtobufObjectGenerator.java index d4f066c6249..0a0df8afa24 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionProtobufObjectGenerator.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionProtobufObjectGenerator.java @@ -23,22 +23,28 @@ import org.sonar.api.server.ServerSide; import org.sonar.db.issue.IssueDto; import org.sonar.db.protobuf.DbIssues; import org.sonarqube.ws.Common; -import org.sonarqube.ws.Issues; + +import static org.sonarqube.ws.Issues.IssueLite; +import static org.sonarqube.ws.Issues.IssuesPullQueryTimestamp; +import static org.sonarqube.ws.Issues.Location; +import static org.sonarqube.ws.Issues.TextRange; @ServerSide -public class PullActionProtobufObjectGenerator { +public class PullActionProtobufObjectGenerator implements ProtobufObjectGenerator { - Issues.IssuesPullQueryTimestamp generateTimestampMessage(long timestamp) { - Issues.IssuesPullQueryTimestamp.Builder responseBuilder = Issues.IssuesPullQueryTimestamp.newBuilder(); + @Override + public IssuesPullQueryTimestamp generateTimestampMessage(long timestamp) { + IssuesPullQueryTimestamp.Builder responseBuilder = IssuesPullQueryTimestamp.newBuilder(); responseBuilder.setQueryTimestamp(timestamp); return responseBuilder.build(); } - Issues.IssueLite generateIssueMessage(IssueDto issueDto) { - Issues.IssueLite.Builder issueBuilder = Issues.IssueLite.newBuilder(); + @Override + public IssueLite generateIssueMessage(IssueDto issueDto) { + IssueLite.Builder issueBuilder = IssueLite.newBuilder(); DbIssues.Locations mainLocation = issueDto.parseLocations(); - Issues.Location.Builder locationBuilder = Issues.Location.newBuilder(); + Location.Builder locationBuilder = Location.newBuilder(); if (issueDto.getMessage() != null) { locationBuilder.setMessage(issueDto.getMessage()); } @@ -46,10 +52,10 @@ public class PullActionProtobufObjectGenerator { locationBuilder.setFilePath(issueDto.getFilePath()); } if (mainLocation != null) { - Issues.TextRange textRange = buildTextRange(mainLocation); + TextRange textRange = buildTextRange(mainLocation); locationBuilder.setTextRange(textRange); } - Issues.Location location = locationBuilder.build(); + Location location = locationBuilder.build(); issueBuilder.setKey(issueDto.getKey()); issueBuilder.setCreationDate(issueDto.getCreatedAt()); @@ -65,24 +71,11 @@ public class PullActionProtobufObjectGenerator { return issueBuilder.build(); } - Issues.IssueLite generateClosedIssueMessage(String uuid) { - Issues.IssueLite.Builder issueBuilder = Issues.IssueLite.newBuilder(); + @Override + public IssueLite generateClosedIssueMessage(String uuid) { + IssueLite.Builder issueBuilder = IssueLite.newBuilder(); issueBuilder.setKey(uuid); issueBuilder.setClosed(true); return issueBuilder.build(); } - - private static Issues.TextRange buildTextRange(DbIssues.Locations mainLocation) { - int startLine = mainLocation.getTextRange().getStartLine(); - int endLine = mainLocation.getTextRange().getEndLine(); - int startOffset = mainLocation.getTextRange().getStartOffset(); - int endOffset = mainLocation.getTextRange().getEndOffset(); - - return Issues.TextRange.newBuilder() - .setHash(mainLocation.getChecksum()) - .setStartLine(startLine) - .setEndLine(endLine) - .setStartLineOffset(startOffset) - .setEndLineOffset(endOffset).build(); - } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionResponseWriter.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionResponseWriter.java index 764afc5ccc2..0df9627f5f4 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionResponseWriter.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullActionResponseWriter.java @@ -19,35 +19,35 @@ */ package org.sonar.server.issue.ws.pull; +import com.google.protobuf.AbstractMessageLite; import java.io.IOException; import java.io.OutputStream; import java.util.List; import org.sonar.api.server.ServerSide; import org.sonar.api.utils.System2; import org.sonar.db.issue.IssueDto; -import org.sonarqube.ws.Issues; @ServerSide public class PullActionResponseWriter { private final System2 system2; - private final PullActionProtobufObjectGenerator pullActionProtobufObjectGenerator; + private final ProtobufObjectGenerator protobufObjectGenerator; - public PullActionResponseWriter(System2 system2, PullActionProtobufObjectGenerator pullActionProtobufObjectGenerator) { + public PullActionResponseWriter(System2 system2, ProtobufObjectGenerator protobufObjectGenerator) { this.system2 = system2; - this.pullActionProtobufObjectGenerator = pullActionProtobufObjectGenerator; + this.protobufObjectGenerator = protobufObjectGenerator; } public void appendTimestampToResponse(OutputStream outputStream) throws IOException { - Issues.IssuesPullQueryTimestamp issuesPullQueryTimestamp = pullActionProtobufObjectGenerator.generateTimestampMessage(system2.now()); - issuesPullQueryTimestamp.writeDelimitedTo(outputStream); + AbstractMessageLite messageLite = protobufObjectGenerator.generateTimestampMessage(system2.now()); + messageLite.writeDelimitedTo(outputStream); } public void appendIssuesToResponse(List<IssueDto> issueDtos, OutputStream outputStream) { try { for (IssueDto issueDto : issueDtos) { - Issues.IssueLite issueLite = pullActionProtobufObjectGenerator.generateIssueMessage(issueDto); - issueLite.writeDelimitedTo(outputStream); + AbstractMessageLite messageLite = protobufObjectGenerator.generateIssueMessage(issueDto); + messageLite.writeDelimitedTo(outputStream); } outputStream.flush(); } catch (IOException e) { @@ -58,8 +58,8 @@ public class PullActionResponseWriter { public void appendClosedIssuesUuidsToResponse(List<String> closedIssuesUuids, OutputStream outputStream) throws IOException { for (String uuid : closedIssuesUuids) { - Issues.IssueLite issueLite = pullActionProtobufObjectGenerator.generateClosedIssueMessage(uuid); - issueLite.writeDelimitedTo(outputStream); + AbstractMessageLite messageLite = protobufObjectGenerator.generateClosedIssueMessage(uuid); + messageLite.writeDelimitedTo(outputStream); } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullTaintActionProtobufObjectGenerator.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullTaintActionProtobufObjectGenerator.java new file mode 100644 index 00000000000..bef5de03c82 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/pull/PullTaintActionProtobufObjectGenerator.java @@ -0,0 +1,175 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.pull; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.sonar.api.server.ServerSide; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.issue.IssueDto; +import org.sonar.db.protobuf.DbIssues; +import org.sonar.db.rule.SeverityUtil; +import org.sonar.server.user.UserSession; +import org.sonarqube.ws.Common; +import org.sonarqube.ws.Issues; + +import static org.sonar.db.protobuf.DbIssues.Locations; +import static org.sonarqube.ws.Issues.TaintVulnerabilityLite; +import static org.sonarqube.ws.Issues.TaintVulnerabilityPullQueryTimestamp; + +@ServerSide +public class PullTaintActionProtobufObjectGenerator implements ProtobufObjectGenerator { + private final DbClient dbClient; + private final UserSession userSession; + private Map<String, ComponentDto> componentsMap; + + public PullTaintActionProtobufObjectGenerator(DbClient dbClient, UserSession userSession) { + this.dbClient = dbClient; + this.userSession = userSession; + } + + @Override + public TaintVulnerabilityPullQueryTimestamp generateTimestampMessage(long timestamp) { + refreshComponents(); + TaintVulnerabilityPullQueryTimestamp.Builder responseBuilder = TaintVulnerabilityPullQueryTimestamp.newBuilder(); + responseBuilder.setQueryTimestamp(timestamp); + return responseBuilder.build(); + } + + @Override + public TaintVulnerabilityLite generateIssueMessage(IssueDto issueDto) { + TaintVulnerabilityLite.Builder taintBuilder = TaintVulnerabilityLite.newBuilder(); + Locations locations = issueDto.parseLocations(); + + if (componentsMap == null) { + refreshComponents(); + } + + Issues.Location.Builder locationBuilder = Issues.Location.newBuilder(); + if (issueDto.getMessage() != null) { + locationBuilder.setMessage(issueDto.getMessage()); + } + if (issueDto.getFilePath() != null) { + locationBuilder.setFilePath(issueDto.getFilePath()); + } + if (locations != null) { + Issues.TextRange textRange = buildTextRange(locations); + locationBuilder.setTextRange(textRange); + getFlows(taintBuilder, locations, issueDto); + } + + taintBuilder.setAssignedToSubscribedUser(issueDto.getAssigneeUuid() != null && + issueDto.getAssigneeUuid().equals(userSession.getUuid())); + + taintBuilder.setKey(issueDto.getKey()); + taintBuilder.setCreationDate(issueDto.getCreatedAt()); + taintBuilder.setResolved(issueDto.getStatus().equals(org.sonar.api.issue.Issue.STATUS_RESOLVED)); + taintBuilder.setRuleKey(issueDto.getRuleKey().toString()); + if (issueDto.getPriority() != null) { + taintBuilder.setSeverity(SeverityUtil.getSeverityFromOrdinal(issueDto.getPriority())); + } + taintBuilder.setType(Common.RuleType.forNumber(issueDto.getType()).name()); + taintBuilder.setClosed(false); + taintBuilder.setMainLocation(locationBuilder.build()); + + return taintBuilder.build(); + } + + @Override + public TaintVulnerabilityLite generateClosedIssueMessage(String uuid) { + TaintVulnerabilityLite.Builder taintBuilder = TaintVulnerabilityLite.newBuilder(); + taintBuilder.setKey(uuid); + taintBuilder.setClosed(true); + return taintBuilder.build(); + } + + private void getFlows(TaintVulnerabilityLite.Builder taintBuilder, Locations locations, IssueDto issueDto) { + List<Issues.Flow> flows = new ArrayList<>(); + + for (DbIssues.Flow f : locations.getFlowList()) { + Set<String> componentUuids = new HashSet<>(); + + Issues.Flow.Builder builder = Issues.Flow.newBuilder(); + List<Issues.Location> flowLocations = new ArrayList<>(); + getComponentUuids(f, componentUuids); + + for (DbIssues.Location l : f.getLocationList()) { + Issues.Location.Builder flowLocationBuilder = Issues.Location + .newBuilder() + .setMessage(l.getMsg()) + .setTextRange(buildTextRange(l)); + if (l.hasComponentId() && componentsMap.containsKey(l.getComponentId())) { + flowLocationBuilder.setFilePath(componentsMap.get(l.getComponentId()).path()); + } else { + flowLocationBuilder.setFilePath(issueDto.getFilePath()); + } + flowLocations.add(flowLocationBuilder.build()); + } + builder.addAllLocations(flowLocations); + flows.add(builder.build()); + + taintBuilder.addAllFlows(flows); + } + } + + private void getComponentUuids(DbIssues.Flow f, Set<String> componentUuids) { + for (DbIssues.Location l : f.getLocationList()) { + if (l.hasComponentId() && !componentsMap.containsKey(l.getComponentId())) { + componentUuids.add(l.getComponentId()); + } + } + + if (!componentUuids.isEmpty()) { + componentsMap.putAll(getLocationComponents(componentUuids)); + } + } + + private static Issues.TextRange buildTextRange(DbIssues.Location location) { + int startLine = location.getTextRange().getStartLine(); + int endLine = location.getTextRange().getEndLine(); + int startOffset = location.getTextRange().getStartOffset(); + int endOffset = location.getTextRange().getEndOffset(); + + return Issues.TextRange.newBuilder() + .setHash(location.getChecksum()) + .setStartLine(startLine) + .setEndLine(endLine) + .setStartLineOffset(startOffset) + .setEndLineOffset(endOffset).build(); + } + + private void refreshComponents() { + componentsMap = new HashMap<>(); + } + + private Map<String, ComponentDto> getLocationComponents(Set<String> components) { + try (DbSession dbSession = dbClient.openSession(false)) { + return dbClient.componentDao().selectByUuids(dbSession, components) + .stream().collect(Collectors.toMap(ComponentDto::uuid, c -> c)); + } + } +} diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/pull-taint-example.proto b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/pull-taint-example.proto new file mode 100644 index 00000000000..fc9d872c01a --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/pull-taint-example.proto @@ -0,0 +1,34 @@ +# The response contains a single protocol buffer message: TaintVulnerabilityPullQueryTimestamp followed by 0..n number of TaintLite protocol buffer messages. +message TaintVulnerabilityPullQueryTimestamp { + required int64 queryTimestamp = 1; +} + +message TaintLite { + required string key = 1; + optional int64 creationDate = 2; + optional bool resolved = 3; + optional string ruleKey = 4; + optional string severity = 5; + optional string type = 6; + optional Location mainLocation = 7; + optional bool closed = 8; + optional Flow flows = 9; +} + +message Location { + optional string filePath = 1; + optional string message = 2; + optional TextRange textRange = 3; +} + +message Flow { + repeated Location locations = 1; +} + +message TextRange { + optional int32 startLine = 1; + optional int32 startLineOffset = 2; + optional int32 endLine = 3; + optional int32 endLineOffset = 4; + optional string hash = 5; +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/PullActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/PullActionTest.java index 0f12633ce0d..bb19b9ca648 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/PullActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/PullActionTest.java @@ -43,7 +43,6 @@ import org.sonar.server.component.ComponentFinder; import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.issue.ws.pull.PullActionProtobufObjectGenerator; -import org.sonar.server.issue.ws.pull.PullActionResponseWriter; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.TestRequest; import org.sonar.server.ws.TestResponse; @@ -78,14 +77,13 @@ public class PullActionTest { private final System2 system2 = mock(System2.class); private final PullActionProtobufObjectGenerator pullActionProtobufObjectGenerator = new PullActionProtobufObjectGenerator(); - private final PullActionResponseWriter pullActionResponseWriter = new PullActionResponseWriter(system2, pullActionProtobufObjectGenerator); private final ResourceTypesRule resourceTypes = new ResourceTypesRule().setRootQualifiers(Qualifiers.PROJECT); private final ComponentFinder componentFinder = new ComponentFinder(db.getDbClient(), resourceTypes); private final IssueDbTester issueDbTester = new IssueDbTester(db); private final ComponentDbTester componentDbTester = new ComponentDbTester(db); - private final PullAction underTest = new PullAction(db.getDbClient(), userSession, pullActionResponseWriter, componentFinder); + private final PullAction underTest = new PullAction(system2, componentFinder, db.getDbClient(), userSession, pullActionProtobufObjectGenerator); private final WsActionTester tester = new WsActionTester(underTest); private RuleDto correctRule, incorrectRule; @@ -159,7 +157,7 @@ public class PullActionTest { loginWithBrowsePermission(issueDto); TestRequest request = tester.newRequest() - .setParam("projectKey", issueDto.getProjectKey()) + .setParam("projectKey", issueDto.getProjectKey()) .setParam("branchName", "non-existent-branch"); assertThatThrownBy(request::execute) @@ -168,6 +166,18 @@ public class PullActionTest { } @Test + public void givenTaintRuleRepository_throwException() { + TestRequest request = tester.newRequest() + .setParam("projectKey", "project-key") + .setParam("branchName", "branch-name") + .setParam("ruleRepositories", "javasecurity"); + + assertThatThrownBy(request::execute) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Incorrect rule repositories list: it should only include repositories that define Issues, and no Taint Vulnerabilities"); + } + + @Test public void givenValidProjectKeyAndOneIssueOnBranch_returnOneIssue() throws IOException { DbCommons.TextRange textRange = DbCommons.TextRange.newBuilder() .setStartLine(1) @@ -179,7 +189,6 @@ public class PullActionTest { .setChecksum("hash") .setTextRange(textRange); - RuleDto rule = db.rules().insertIssueRule(r -> r.setRepositoryKey("java").setRuleKey("S1000")); IssueDto issueDto = issueDbTester.insertIssue(rule, p -> p.setSeverity("MINOR") .setManualSeverity(true) @@ -196,10 +205,10 @@ public class PullActionTest { TestResponse response = request.execute(); List<Issues.IssueLite> issues = readAllIssues(response); - Issues.IssueLite issueLite = issues.get(0); assertThat(issues).hasSize(1); + Issues.IssueLite issueLite = issues.get(0); assertThat(issueLite.getKey()).isEqualTo(issueDto.getKey()); assertThat(issueLite.getUserSeverity()).isEqualTo("MINOR"); assertThat(issueLite.getCreationDate()).isEqualTo(NOW); @@ -219,6 +228,38 @@ public class PullActionTest { } @Test + public void givenValidProjectKeyAndOneTaintVulnerabilityOnBranch_returnNoIssues() throws IOException { + DbCommons.TextRange textRange = DbCommons.TextRange.newBuilder() + .setStartLine(1) + .setEndLine(2) + .setStartOffset(3) + .setEndOffset(4) + .build(); + DbIssues.Locations.Builder mainLocation = DbIssues.Locations.newBuilder() + .setChecksum("hash") + .setTextRange(textRange); + + RuleDto rule = db.rules().insertIssueRule(r -> r.setRepositoryKey("javasecurity").setRuleKey("S1000")); + IssueDto issueDto = issueDbTester.insertIssue(rule, p -> p.setSeverity("MINOR") + .setManualSeverity(true) + .setMessage("message") + .setCreatedAt(NOW) + .setStatus(Issue.STATUS_RESOLVED) + .setLocations(mainLocation.build()) + .setType(Common.RuleType.BUG.getNumber())); + loginWithBrowsePermission(issueDto); + + TestRequest request = tester.newRequest() + .setParam("projectKey", issueDto.getProjectKey()) + .setParam("branchName", DEFAULT_BRANCH); + + TestResponse response = request.execute(); + List<Issues.IssueLite> issues = readAllIssues(response); + + assertThat(issues).isEmpty(); + } + + @Test public void givenIssueOnAnotherBranch_returnOneIssue() throws IOException { ComponentDto developBranch = componentDbTester.insertPrivateProjectWithCustomBranch("develop"); ComponentDto developFile = db.components().insertComponent(newFileDto(developBranch)); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/PullTaintActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/PullTaintActionTest.java new file mode 100644 index 00000000000..647e7a41acd --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/PullTaintActionTest.java @@ -0,0 +1,491 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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 java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.issue.Issue; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.utils.System2; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ResourceTypesRule; +import org.sonar.db.issue.IssueDbTester; +import org.sonar.db.issue.IssueDto; +import org.sonar.db.protobuf.DbCommons; +import org.sonar.db.protobuf.DbIssues; +import org.sonar.db.rule.RuleDto; +import org.sonar.db.rule.RuleRepositoryDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.issue.ws.pull.PullTaintActionProtobufObjectGenerator; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.TestResponse; +import org.sonar.server.ws.WsActionTester; +import org.sonarqube.ws.Common; +import org.sonarqube.ws.Issues; + +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.api.web.UserRole.USER; +import static org.sonar.db.component.ComponentTesting.newFileDto; + +public class PullTaintActionTest { + private static final long NOW = 10_000_000_000L; + private static final long PAST = 1_000_000_000L; + + private static final String DEFAULT_BRANCH = "master"; + + @Rule + public DbTester dbTester = DbTester.create(); + + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + + @Rule + public DbTester db = DbTester.create(System2.INSTANCE); + + private final System2 system2 = mock(System2.class); + private final ResourceTypesRule resourceTypes = new ResourceTypesRule().setRootQualifiers(Qualifiers.PROJECT); + private final ComponentFinder componentFinder = new ComponentFinder(db.getDbClient(), resourceTypes); + private final IssueDbTester issueDbTester = new IssueDbTester(db); + private final ComponentDbTester componentDbTester = new ComponentDbTester(db); + + private PullTaintActionProtobufObjectGenerator objectGenerator = new PullTaintActionProtobufObjectGenerator(db.getDbClient(), userSession); + private PullTaintAction underTest = new PullTaintAction(system2, componentFinder, db.getDbClient(), userSession, objectGenerator); + private WsActionTester tester = new WsActionTester(underTest); + + private RuleDto correctRule, incorrectRule; + private ComponentDto correctProject, incorrectProject; + private ComponentDto correctFile, incorrectFile; + + @Before + public void setUp() { + when(system2.now()).thenReturn(NOW); + RuleRepositoryDto repository = new RuleRepositoryDto("javasecurity", "java", "Security SonarAnalyzer"); + db.getDbClient().ruleRepositoryDao().insert(db.getSession(), List.of(repository)); + correctRule = db.rules().insertIssueRule(r -> r.setRepositoryKey("javasecurity").setRuleKey("S1000").setSeverity(3)); + + correctProject = db.components().insertPrivateProject(); + correctFile = db.components().insertComponent(newFileDto(correctProject)); + + incorrectRule = db.rules().insertIssueRule(); + incorrectProject = db.components().insertPrivateProject(); + incorrectFile = db.components().insertComponent(newFileDto(incorrectProject)); + + + } + + @Test + public void givenMissingParams_expectIllegalArgumentException() { + TestRequest request = tester.newRequest(); + + assertThatThrownBy(() -> request.executeProtobuf(Issues.IssuesPullQueryTimestamp.class)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void givenNotExistingProjectKey_throwException() { + TestRequest request = tester.newRequest() + .setParam("projectKey", "projectKey") + .setParam("branchName", DEFAULT_BRANCH); + + assertThatThrownBy(request::execute) + .isInstanceOf(NotFoundException.class) + .hasMessage("Project 'projectKey' not found"); + } + + @Test + public void givenValidProjectKeyWithoutPermissionsTo_throwException() { + userSession.logIn(); + + TestRequest request = tester.newRequest() + .setParam("projectKey", correctProject.getKey()) + .setParam("branchName", DEFAULT_BRANCH); + + assertThatThrownBy(request::execute) + .isInstanceOf(ForbiddenException.class) + .hasMessage("Insufficient privileges"); + } + + @Test + public void givenNotExistingBranchKey_throwException() { + DbCommons.TextRange textRange = DbCommons.TextRange.newBuilder() + .setStartLine(1) + .setEndLine(2) + .setStartOffset(3) + .setEndOffset(4) + .build(); + DbIssues.Locations.Builder mainLocation = DbIssues.Locations.newBuilder() + .setChecksum("hash") + .setTextRange(textRange); + + RuleDto rule = db.rules().insertIssueRule(r -> r.setRepositoryKey("java").setRuleKey("S1000")); + IssueDto issueDto = issueDbTester.insertIssue(rule, p -> p.setSeverity("MINOR") + .setManualSeverity(true) + .setMessage("message") + .setCreatedAt(NOW) + .setStatus(Issue.STATUS_RESOLVED) + .setLocations(mainLocation.build()) + .setType(Common.RuleType.BUG.getNumber())); + loginWithBrowsePermission(issueDto); + + TestRequest request = tester.newRequest() + .setParam("projectKey", issueDto.getProjectKey()) + .setParam("branchName", "non-existent-branch"); + + assertThatThrownBy(request::execute) + .isInstanceOf(NotFoundException.class) + .hasMessage(format("Branch 'non-existent-branch' in project '%s' not found", issueDto.getProjectKey())); + } + + @Test + public void givenValidProjectKeyAndOneNormalIssueOnBranch_returnNoTaintVulnerabilities() throws IOException { + DbCommons.TextRange textRange = DbCommons.TextRange.newBuilder() + .setStartLine(1) + .setEndLine(2) + .setStartOffset(3) + .setEndOffset(4) + .build(); + DbIssues.Locations.Builder mainLocation = DbIssues.Locations.newBuilder() + .setChecksum("hash") + .setTextRange(textRange); + + RuleDto rule = db.rules().insertIssueRule(r -> r.setRepositoryKey("java").setRuleKey("S1000")); + IssueDto issueDto = issueDbTester.insertIssue(rule, p -> p.setSeverity("MINOR") + .setManualSeverity(true) + .setMessage("message") + .setCreatedAt(NOW) + .setStatus(Issue.STATUS_RESOLVED) + .setLocations(mainLocation.build()) + .setType(Common.RuleType.BUG.getNumber())); + loginWithBrowsePermission(issueDto); + + TestRequest request = tester.newRequest() + .setParam("projectKey", issueDto.getProjectKey()) + .setParam("branchName", DEFAULT_BRANCH); + + TestResponse response = request.execute(); + List<Issues.TaintVulnerabilityLite> taints = readAllTaint(response); + + assertThat(taints).isEmpty(); + } + + @Test + public void givenValidProjectKeyAndOneTaintOnBranch_returnOneTaint_WithMetadataSeverity() throws IOException { + loginWithBrowsePermission(correctProject.projectUuid(), correctFile.uuid()); + DbCommons.TextRange textRange = DbCommons.TextRange.newBuilder() + .setStartLine(1) + .setEndLine(2) + .setStartOffset(3) + .setEndOffset(4) + .build(); + DbIssues.Locations.Builder mainLocation = DbIssues.Locations.newBuilder() + .setChecksum("hash") + .setTextRange(textRange); + + IssueDto issueDto = issueDbTester.insertIssue(p -> p.setSeverity("MINOR") + .setRule(correctRule) + .setProject(correctProject) + .setComponent(correctFile) + .setAssigneeUuid(userSession.getUuid()) + .setManualSeverity(true) + .setMessage("message") + .setCreatedAt(NOW) + .setStatus(Issue.STATUS_OPEN) + .setLocations(mainLocation.build()) + .setType(Common.RuleType.VULNERABILITY.getNumber())); + + TestRequest request = tester.newRequest() + .setParam("projectKey", issueDto.getProjectKey()) + .setParam("branchName", DEFAULT_BRANCH); + + TestResponse response = request.execute(); + List<Issues.TaintVulnerabilityLite> taints = readAllTaint(response); + Issues.TaintVulnerabilityLite taintLite = taints.get(0); + + assertThat(taints).hasSize(1); + + assertThat(taintLite.getKey()).isEqualTo(issueDto.getKey()); + assertThat(taintLite.getSeverity()).isEqualTo("CRITICAL"); + assertThat(taintLite.getCreationDate()).isEqualTo(NOW); + assertThat(taintLite.getResolved()).isFalse(); + assertThat(taintLite.getRuleKey()).isEqualTo("javasecurity:S1000"); + assertThat(taintLite.getType()).isEqualTo(Common.RuleType.forNumber(issueDto.getType()).name()); + assertThat(taintLite.getAssignedToSubscribedUser()).isTrue(); + + Issues.Location location = taintLite.getMainLocation(); + assertThat(location.getMessage()).isEqualTo(issueDto.getMessage()); + + Issues.TextRange locationTextRange = location.getTextRange(); + assertThat(locationTextRange.getStartLine()).isEqualTo(1); + assertThat(locationTextRange.getEndLine()).isEqualTo(2); + assertThat(locationTextRange.getStartLineOffset()).isEqualTo(3); + assertThat(locationTextRange.getEndLineOffset()).isEqualTo(4); + assertThat(locationTextRange.getHash()).isEqualTo("hash"); + } + + @Test + public void givenTaintOnAnotherBranch_returnOneTaint() throws IOException { + ComponentDto developBranch = componentDbTester.insertPrivateProjectWithCustomBranch("develop"); + ComponentDto developFile = db.components().insertComponent(newFileDto(developBranch)); + generateTaints(correctRule, developBranch, developFile, 1); + loginWithBrowsePermission(developBranch.uuid(), developFile.uuid()); + + TestRequest request = tester.newRequest() + .setParam("projectKey", developBranch.getKey()) + .setParam("branchName", "develop"); + + TestResponse response = request.execute(); + List<Issues.TaintVulnerabilityLite> taints = readAllTaint(response); + + assertThat(taints).hasSize(1); + } + + @Test + public void given15TaintsInTheTable_returnOnly10ThatBelongToProject() throws IOException { + loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid()); + generateTaints(correctRule, correctProject, correctFile, 10); + generateTaints(incorrectRule, incorrectProject, incorrectFile, 5); + + TestRequest request = tester.newRequest() + .setParam("projectKey", correctProject.getKey()) + .setParam("branchName", DEFAULT_BRANCH); + + TestResponse response = request.execute(); + List<Issues.TaintVulnerabilityLite> taints = readAllTaint(response); + + assertThat(taints).hasSize(10); + } + + @Test + public void givenNoTaintsBelongToTheProject_return0Taints() throws IOException { + loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid()); + generateTaints(incorrectRule, incorrectProject, incorrectFile, 5); + + TestRequest request = tester.newRequest() + .setParam("projectKey", correctProject.getKey()) + .setParam("branchName", DEFAULT_BRANCH); + + TestResponse response = request.execute(); + List<Issues.TaintVulnerabilityLite> taints = readAllTaint(response); + + assertThat(taints).isEmpty(); + } + + @Test + public void testLanguagesParam_return1Taint() throws IOException { + loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid()); + RuleDto javaRule = db.rules().insert(r -> r.setLanguage("java").setRepositoryKey("javasecurity")); + RuleDto javascriptRule = db.rules().insert(r -> r.setLanguage("javascript").setRepositoryKey("javasecurity")); + + IssueDto javaIssue = issueDbTester.insertIssue(p -> p.setSeverity("MINOR") + .setManualSeverity(true) + .setMessage("openIssue") + .setCreatedAt(NOW) + .setRule(javaRule) + .setRuleUuid(javaRule.getUuid()) + .setStatus(Issue.STATUS_OPEN) + .setLanguage("java") + .setProject(correctProject) + .setComponent(correctFile) + .setType(Common.RuleType.VULNERABILITY.getNumber())); + + issueDbTester.insertIssue(p -> p.setSeverity("MINOR") + .setManualSeverity(true) + .setMessage("openIssue") + .setCreatedAt(NOW) + .setRule(javascriptRule) + .setRuleUuid(javascriptRule.getUuid()) + .setStatus(Issue.STATUS_OPEN) + .setLanguage("java") + .setProject(correctProject) + .setComponent(correctFile) + .setType(Common.RuleType.VULNERABILITY.getNumber())); + + TestRequest request = tester.newRequest() + .setParam("projectKey", correctProject.getKey()) + .setParam("branchName", DEFAULT_BRANCH) + .setParam("languages", "java"); + + TestResponse response = request.execute(); + List<Issues.TaintVulnerabilityLite> taints = readAllTaint(response); + + assertThat(taints).hasSize(1); + assertThat(taints.get(0).getKey()).isEqualTo(javaIssue.getKey()); + } + + @Test + public void testLanguagesParam_givenWrongLanguage_return0Taints() throws IOException { + loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid()); + RuleDto javascriptRule = db.rules().insert(r -> r.setLanguage("jssecurity")); + + issueDbTester.insertIssue(p -> p.setSeverity("MINOR") + .setManualSeverity(true) + .setMessage("openIssue") + .setCreatedAt(NOW) + .setRule(javascriptRule) + .setRuleUuid(javascriptRule.getUuid()) + .setStatus(Issue.STATUS_OPEN) + .setProject(correctProject) + .setComponent(correctFile) + .setType(2)); + + TestRequest request = tester.newRequest() + .setParam("projectKey", correctProject.getKey()) + .setParam("branchName", DEFAULT_BRANCH) + .setParam("languages", "java"); + + TestResponse response = request.execute(); + List<Issues.TaintVulnerabilityLite> taints = readAllTaint(response); + + assertThat(taints).isEmpty(); + } + + @Test + public void given1TaintAnd1NormalIssue_return1Taint() throws IOException { + loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid()); + RuleDto javaRule = db.rules().insert(r -> r.setRepositoryKey("javasecurity")); + RuleDto javaScriptRule = db.rules().insert(r -> r.setRepositoryKey("javascript")); + + IssueDto issueDto = issueDbTester.insertIssue(p -> p.setSeverity("MINOR") + .setManualSeverity(true) + .setMessage("openIssue") + .setCreatedAt(NOW) + .setRule(javaRule) + .setStatus(Issue.STATUS_OPEN) + .setProject(correctProject) + .setComponent(correctFile) + .setType(2)); + + //this one should not be returned - it is a normal issue, no taint + issueDbTester.insertIssue(p -> p.setSeverity("MINOR") + .setManualSeverity(true) + .setMessage("openIssue") + .setCreatedAt(NOW) + .setRule(javaScriptRule) + .setStatus(Issue.STATUS_OPEN) + .setProject(correctProject) + .setComponent(correctFile) + .setType(Common.RuleType.VULNERABILITY.getNumber())); + + TestRequest request = tester.newRequest() + .setParam("projectKey", correctProject.getKey()) + .setParam("branchName", DEFAULT_BRANCH); + + TestResponse response = request.execute(); + List<Issues.TaintVulnerabilityLite> taints = readAllTaint(response); + + assertThat(taints).hasSize(1); + assertThat(taints.get(0).getKey()).isEqualTo(issueDto.getKey()); + } + + @Test + public void inIncrementalModeReturnClosedIssues() throws IOException { + IssueDto openIssue = issueDbTester.insertIssue(p -> p.setSeverity("MINOR") + .setRule(correctRule) + .setManualSeverity(true) + .setMessage("openIssue") + .setCreatedAt(NOW) + .setStatus(Issue.STATUS_OPEN) + .setType(Common.RuleType.BUG.getNumber())); + + issueDbTester.insertIssue(p -> p.setSeverity("MINOR") + .setRule(correctRule) + .setMessage("closedIssue") + .setCreatedAt(NOW) + .setStatus(Issue.STATUS_CLOSED) + .setType(Common.RuleType.BUG.getNumber()) + .setComponentUuid(openIssue.getComponentUuid()) + .setProjectUuid(openIssue.getProjectUuid()) + .setIssueUpdateTime(PAST) + .setIssueCreationTime(PAST)); + + issueDbTester.insertIssue(p -> p.setSeverity("MINOR") + .setRule(incorrectRule) + .setMessage("closedIssue") + .setCreatedAt(NOW) + .setStatus(Issue.STATUS_CLOSED) + .setType(Common.RuleType.BUG.getNumber()) + .setComponentUuid(openIssue.getComponentUuid()) + .setProjectUuid(openIssue.getProjectUuid()) + .setIssueUpdateTime(PAST) + .setIssueCreationTime(PAST)); + + loginWithBrowsePermission(openIssue); + + TestRequest request = tester.newRequest() + .setParam("projectKey", openIssue.getProjectKey()) + .setParam("branchName", DEFAULT_BRANCH) + .setParam("changedSince", PAST + ""); + + TestResponse response = request.execute(); + List<Issues.TaintVulnerabilityLite> taints = readAllTaint(response); + + assertThat(taints).hasSize(2); + } + + private void generateTaints(RuleDto rule, ComponentDto project, ComponentDto file, int numberOfIssues) { + for (int j = 0; j < numberOfIssues; j++) { + issueDbTester.insert(i -> i.setProject(project) + .setRule(rule) + .setComponent(file) + .setStatus(Issue.STATUS_OPEN) + .setType(3)); + } + } + + private List<Issues.TaintVulnerabilityLite> readAllTaint(TestResponse response) throws IOException { + List<Issues.TaintVulnerabilityLite> taints = new ArrayList<>(); + InputStream inputStream = response.getInputStream(); + Issues.TaintVulnerabilityPullQueryTimestamp.parseDelimitedFrom(inputStream); + + while (inputStream.available() > 0) { + taints.add(Issues.TaintVulnerabilityLite.parseDelimitedFrom(inputStream)); + } + + return taints; + } + + private void loginWithBrowsePermission(IssueDto issueDto) { + loginWithBrowsePermission(issueDto.getProjectUuid(), issueDto.getComponentUuid()); + } + + private void loginWithBrowsePermission(String projectUuid, String componentUuid) { + UserDto user = dbTester.users().insertUser("john"); + userSession.logIn(user) + .addProjectPermission(USER, + db.getDbClient().componentDao().selectByUuid(dbTester.getSession(), projectUuid).get(), + db.getDbClient().componentDao().selectByUuid(dbTester.getSession(), componentUuid).get()); + } + +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetrieverTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetrieverTest.java index 30233c93ec6..76046a4c40b 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetrieverTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullActionIssuesRetrieverTest.java @@ -46,7 +46,7 @@ public class PullActionIssuesRetrieverTest { private final List<String> ruleRepositories = List.of("js-security", "java"); private final Long defaultChangedSince = 1_000_000L; - private final IssueQueryParams queryParams = new IssueQueryParams(branchUuid, languages, ruleRepositories, false, defaultChangedSince); + private final IssueQueryParams queryParams = new IssueQueryParams(branchUuid, languages, ruleRepositories, null, false, defaultChangedSince); private final IssueDao issueDao = mock(IssueDao.class); @Before diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullTaintActionResponseWriterTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullTaintActionResponseWriterTest.java new file mode 100644 index 00000000000..1d84e1267ef --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/pull/PullTaintActionResponseWriterTest.java @@ -0,0 +1,118 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 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.pull; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.List; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.utils.System2; +import org.sonar.db.DbTester; +import org.sonar.db.issue.IssueDto; +import org.sonar.db.protobuf.DbCommons; +import org.sonar.db.protobuf.DbIssues; +import org.sonar.server.tester.UserSessionRule; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class PullTaintActionResponseWriterTest { + @Rule + public DbTester db = DbTester.create(System2.INSTANCE); + + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + + private final System2 system2 = mock(System2.class); + private final PullTaintActionProtobufObjectGenerator protobufObjectGenerator = new PullTaintActionProtobufObjectGenerator(db.getDbClient(), + userSession); + + private final PullActionResponseWriter underTest = new PullActionResponseWriter(system2, protobufObjectGenerator); + + @Before + public void before() { + when(system2.now()).thenReturn(1_000_000L); + } + + @Test + public void appendIssuesToResponse_outputStreamIsCalledAtLeastOnce() throws IOException { + OutputStream outputStream = mock(OutputStream.class); + IssueDto issueDto = new IssueDto(); + issueDto.setFilePath("filePath"); + issueDto.setKee("key"); + issueDto.setStatus("OPEN"); + issueDto.setRuleKey("repo", "rule"); + DbIssues.Locations locations = DbIssues.Locations.newBuilder() + .setTextRange(range(2, 3)) + .addFlow(newFlow(newLocation(4, 5))) + .addFlow(newFlow(newLocation(6, 7, "another-component"))) + .build(); + + issueDto.setLocations(locations); + + underTest.appendIssuesToResponse(List.of(issueDto), outputStream); + + verify(outputStream, atLeastOnce()).write(any(byte[].class), anyInt(), anyInt()); + } + + @Test + public void appendClosedIssuesToResponse_outputStreamIsCalledAtLeastOnce() throws IOException { + OutputStream outputStream = mock(OutputStream.class); + + underTest.appendClosedIssuesUuidsToResponse(List.of("uuid", "uuid2"), outputStream); + + verify(outputStream, atLeastOnce()).write(any(byte[].class), anyInt(), anyInt()); + } + + @Test + public void appendTimestampToResponse_outputStreamIsCalledAtLeastOnce() throws IOException { + OutputStream outputStream = mock(OutputStream.class); + + underTest.appendTimestampToResponse(outputStream); + + verify(outputStream, atLeastOnce()).write(any(byte[].class), anyInt(), anyInt()); + } + + private static DbIssues.Location newLocation(int startLine, int endLine) { + return DbIssues.Location.newBuilder().setTextRange(range(startLine, endLine)).build(); + } + + private static DbIssues.Location newLocation(int startLine, int endLine, String componentUuid) { + return DbIssues.Location.newBuilder().setTextRange(range(startLine, endLine)).setComponentId(componentUuid).build(); + } + + + private static org.sonar.db.protobuf.DbCommons.TextRange range(int startLine, int endLine) { + return DbCommons.TextRange.newBuilder().setStartLine(startLine).setEndLine(endLine).build(); + } + + private static DbIssues.Flow newFlow(DbIssues.Location... locations) { + DbIssues.Flow.Builder builder = DbIssues.Flow.newBuilder(); + Arrays.stream(locations).forEach(builder::addLocation); + return builder.build(); + } +} |