import com.google.common.collect.ImmutableSet;
import java.util.Arrays;
import java.util.Collection;
-import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.String.format;
import static java.util.Collections.singleton;
+import static java.util.Collections.singletonList;
import static java.util.Optional.ofNullable;
import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
import static org.sonar.api.issue.Issue.RESOLUTION_SAFE;
import static org.sonar.core.util.stream.MoreCollectors.toList;
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
import static org.sonar.server.security.SecurityStandards.fromSecurityStandards;
+import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
import static org.sonar.server.ws.WsUtils.writeProtobuf;
import static org.sonarqube.ws.WsUtils.nullToEmpty;
private static final String PARAM_STATUS = "status";
private static final String PARAM_RESOLUTION = "resolution";
private static final String PARAM_HOTSPOTS = "hotspots";
+ private static final String PARAM_BRANCH = "branch";
+ private static final String PARAM_PULL_REQUEST = "pullRequest";
private final DbClient dbClient;
private final UserSession userSession;
"Key of the project. This parameter is required unless %s is provided.",
PARAM_HOTSPOTS))
.setExampleValue(KEY_PROJECT_EXAMPLE_001);
+ action.createParam(PARAM_BRANCH)
+ .setDescription("Branch key")
+ .setExampleValue(KEY_BRANCH_EXAMPLE_001)
+ .setInternal(true);
+ action.createParam(PARAM_PULL_REQUEST)
+ .setDescription("Pull request id")
+ .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001)
+ .setInternal(true);
action.createParam(PARAM_HOTSPOTS)
.setDescription(format(
"Comma-separated list of Security Hotspot keys. This parameter is required unless %s is provided.",
Set<String> hotspotKeys = hotspotKeysList == null ? ImmutableSet.of() : hotspotKeysList.stream().collect(MoreCollectors.toSet(hotspotKeysList.size()));
return new WsRequest(
request.mandatoryParamAsInt(PAGE), request.mandatoryParamAsInt(PAGE_SIZE),
- request.param(PARAM_PROJECT_KEY), hotspotKeys,
+ request.param(PARAM_PROJECT_KEY), request.param(PARAM_BRANCH), request.param(PARAM_PULL_REQUEST),
+ hotspotKeys,
request.param(PARAM_STATUS), request.param(PARAM_RESOLUTION));
}
private static void validateParameters(WsRequest wsRequest) {
+ Optional<String> projectKey = wsRequest.getProjectKey();
+ Optional<String> branch = wsRequest.getBranch();
+ Optional<String> pullRequest = wsRequest.getPullRequest();
+ Set<String> hotspotKeys = wsRequest.getHotspotKeys();
+
checkArgument(
- wsRequest.getProjectKey().isPresent() || !wsRequest.getHotspotKeys().isEmpty(),
+ projectKey.isPresent() || !hotspotKeys.isEmpty(),
"A value must be provided for either parameter '%s' or parameter '%s'", PARAM_PROJECT_KEY, PARAM_HOTSPOTS);
- checkArgument(!wsRequest.getStatus().isPresent() || wsRequest.getHotspotKeys().isEmpty(),
+ checkArgument(
+ !branch.isPresent() || projectKey.isPresent(),
+ "Parameter '%s' must be used with parameter '%s'", PARAM_BRANCH, PARAM_PROJECT_KEY);
+ checkArgument(
+ !pullRequest.isPresent() || projectKey.isPresent(),
+ "Parameter '%s' must be used with parameter '%s'", PARAM_PULL_REQUEST, PARAM_PROJECT_KEY);
+ checkArgument(
+ !(branch.isPresent() && pullRequest.isPresent()),
+ "Only one of parameters '%s' and '%s' can be provided", PARAM_BRANCH, PARAM_PULL_REQUEST);
+
+ Optional<String> status = wsRequest.getStatus();
+ Optional<String> resolution = wsRequest.getResolution();
+ checkArgument(!status.isPresent() || hotspotKeys.isEmpty(),
"Parameter '%s' can't be used with parameter '%s'", PARAM_STATUS, PARAM_HOTSPOTS);
- checkArgument(!wsRequest.getResolution().isPresent() || wsRequest.getHotspotKeys().isEmpty(),
+ checkArgument(!resolution.isPresent() || hotspotKeys.isEmpty(),
"Parameter '%s' can't be used with parameter '%s'", PARAM_RESOLUTION, PARAM_HOTSPOTS);
- wsRequest.getResolution().ifPresent(
- resolution -> checkArgument(wsRequest.getStatus().filter(STATUS_REVIEWED::equals).isPresent(),
+ resolution.ifPresent(
+ r -> checkArgument(status.filter(STATUS_REVIEWED::equals).isPresent(),
"Value '%s' of parameter '%s' can only be provided if value of parameter '%s' is '%s'",
- resolution, PARAM_RESOLUTION, PARAM_STATUS, STATUS_REVIEWED));
+ r, PARAM_RESOLUTION, PARAM_STATUS, STATUS_REVIEWED));
}
private Optional<ComponentDto> getAndValidateProject(DbSession dbSession, WsRequest wsRequest) {
return wsRequest.getProjectKey().map(projectKey -> {
- ComponentDto project = dbClient.componentDao().selectByKey(dbSession, projectKey)
+ ComponentDto project = getProject(dbSession, projectKey, wsRequest.getBranch(), wsRequest.getPullRequest())
.filter(t -> Scopes.PROJECT.equals(t.scope()) && Qualifiers.PROJECT.equals(t.qualifier()))
.filter(ComponentDto::isEnabled)
- .filter(t -> t.getMainBranchProjectUuid() == null)
.orElseThrow(() -> new NotFoundException(format("Project '%s' not found", projectKey)));
userSession.checkComponentPermission(UserRole.USER, project);
return project;
});
}
+ private Optional<ComponentDto> getProject(DbSession dbSession, String projectKey, Optional<String> branch, Optional<String> pullRequest) {
+ if (branch.isPresent()) {
+ return dbClient.componentDao().selectByKeyAndBranch(dbSession, projectKey, branch.get());
+ } else if (pullRequest.isPresent()) {
+ return dbClient.componentDao().selectByKeyAndPullRequest(dbSession, projectKey, pullRequest.get());
+ }
+ return dbClient.componentDao().selectByKey(dbSession, projectKey);
+ }
+
private SearchResponseData searchHotspots(WsRequest wsRequest, DbSession dbSession, Optional<ComponentDto> project, Set<String> hotspotKeys) {
SearchResponse result = doIndexSearch(wsRequest, project, hotspotKeys);
List<String> issueKeys = Arrays.stream(result.getHits().getHits())
.sort(IssueQuery.SORT_HOTSPOTS)
.asc(true);
project.ifPresent(p -> {
- builder.projectUuids(Collections.singletonList(p.uuid()));
builder.organizationUuid(p.getOrganizationUuid());
+
+ if (p.getMainBranchProjectUuid() == null) {
+ builder.mainBranch(true);
+ builder.projectUuids(singletonList(p.uuid()));
+ } else {
+ builder.branchUuid(p.projectUuid());
+ builder.mainBranch(false);
+ }
});
if (!hotspotKeys.isEmpty()) {
builder.issueKeys(hotspotKeys);
private final int page;
private final int index;
private final String projectKey;
+ private final String branch;
+ private final String pullRequest;
private final Set<String> hotspotKeys;
private final String status;
private final String resolution;
- private WsRequest(int page, int index, @Nullable String projectKey, Set<String> hotspotKeys, @Nullable String status, @Nullable String resolution) {
+ private WsRequest(int page, int index,
+ @Nullable String projectKey, @Nullable String branch, @Nullable String pullRequest,
+ Set<String> hotspotKeys,
+ @Nullable String status, @Nullable String resolution) {
this.page = page;
this.index = index;
this.projectKey = projectKey;
+ this.branch = branch;
+ this.pullRequest = pullRequest;
this.hotspotKeys = hotspotKeys;
this.status = status;
this.resolution = resolution;
return ofNullable(projectKey);
}
+ Optional<String> getBranch() {
+ return ofNullable(branch);
+ }
+
+ Optional<String> getPullRequest() {
+ return ofNullable(pullRequest);
+ }
+
Set<String> getHotspotKeys() {
return hotspotKeys;
}
import org.sonar.api.web.UserRole;
import org.sonar.db.DbClient;
import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchType;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ComponentTesting;
import org.sonar.db.issue.IssueDto;
.hasMessage("A value must be provided for either parameter 'projectKey' or parameter 'hotspots'");
}
+ @Test
+ public void fail_with_IAE_if_parameter_branch_is_used_without_parameter_projectKey() {
+ TestRequest request = actionTester.newRequest()
+ .setParam("hotspots", randomAlphabetic(2))
+ .setParam("branch", randomAlphabetic(1));
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Parameter 'branch' must be used with parameter 'projectKey'");
+ }
+
+ @Test
+ public void fail_with_IAE_if_parameter_pullRequest_is_used_without_parameter_projectKey() {
+ TestRequest request = actionTester.newRequest()
+ .setParam("hotspots", randomAlphabetic(2))
+ .setParam("pullRequest", randomAlphabetic(1));
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Parameter 'pullRequest' must be used with parameter 'projectKey'");
+ }
+
+ @Test
+ public void fail_with_IAE_if_both_parameters_pullRequest_and_branch_are_provided() {
+ TestRequest request = actionTester.newRequest()
+ .setParam("projectKey", randomAlphabetic(2))
+ .setParam("branch", randomAlphabetic(1))
+ .setParam("pullRequest", randomAlphabetic(1));
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Only one of parameters 'branch' and 'pullRequest' can be provided");
+ }
+
@Test
@UseDataProvider("badStatuses")
public void fails_with_IAE_if_status_parameter_is_neither_TO_REVIEW_or_REVIEWED(String badStatus) {
SearchWsResponse response = newRequest(project)
.executeProtobuf(SearchWsResponse.class);
+
+ assertThat(response.getHotspotsList()).isEmpty();
}
@Test
.containsOnly(project2.getKey(), file2.getKey());
}
+ @Test
+ public void returns_hotspot_of_branch_or_pullRequest() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+ indexPermissions();
+ ComponentDto branch = dbTester.components().insertProjectBranch(project);
+ ComponentDto pullRequest = dbTester.components().insertProjectBranch(project, t -> t.setBranchType(BranchType.PULL_REQUEST));
+ ComponentDto fileProject = dbTester.components().insertComponent(newFileDto(project));
+ ComponentDto fileBranch = dbTester.components().insertComponent(newFileDto(branch));
+ ComponentDto filePR = dbTester.components().insertComponent(newFileDto(pullRequest));
+ IssueDto[] hotspotProject = IntStream.range(0, 1 + RANDOM.nextInt(10))
+ .mapToObj(i -> {
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ return insertHotspot(project, fileProject, rule);
+ })
+ .toArray(IssueDto[]::new);
+ IssueDto[] hotspotBranch = IntStream.range(0, 1 + RANDOM.nextInt(10))
+ .mapToObj(i -> {
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ return insertHotspot(branch, fileBranch, rule);
+ })
+ .toArray(IssueDto[]::new);
+ IssueDto[] hotspotPR = IntStream.range(0, 1 + RANDOM.nextInt(10))
+ .mapToObj(i -> {
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ return insertHotspot(pullRequest, filePR, rule);
+ })
+ .toArray(IssueDto[]::new);
+ indexIssues();
+
+ SearchWsResponse responseProject = newRequest(project)
+ .executeProtobuf(SearchWsResponse.class);
+ SearchWsResponse responseBranch = newRequest(branch)
+ .executeProtobuf(SearchWsResponse.class);
+ SearchWsResponse responsePR = newRequest(pullRequest)
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(responseProject.getHotspotsList())
+ .extracting(SearchWsResponse.Hotspot::getKey)
+ .containsExactlyInAnyOrder(Arrays.stream(hotspotProject).map(IssueDto::getKey).toArray(String[]::new));
+ assertThat(responseBranch.getHotspotsList())
+ .extracting(SearchWsResponse.Hotspot::getKey)
+ .containsExactlyInAnyOrder(Arrays.stream(hotspotBranch).map(IssueDto::getKey).toArray(String[]::new));
+ assertThat(responsePR.getHotspotsList())
+ .extracting(SearchWsResponse.Hotspot::getKey)
+ .containsExactlyInAnyOrder(Arrays.stream(hotspotPR).map(IssueDto::getKey).toArray(String[]::new));
+ }
+
@Test
public void returns_hotpots_with_any_status_if_no_status_nor_resolution_parameter() {
ComponentDto project = dbTester.components().insertPublicProject();
assertThat(actualProject.getName()).isEqualTo(project.name());
assertThat(actualProject.getLongName()).isEqualTo(project.longName());
assertThat(actualProject.hasPath()).isFalse();
+ assertThat(actualProject.hasBranch()).isFalse();
+ assertThat(actualProject.hasPullRequest()).isFalse();
Component actualDirectory = componentByKey.get(directory.getKey());
assertThat(actualDirectory.getQualifier()).isEqualTo(directory.qualifier());
assertThat(actualDirectory.getName()).isEqualTo(directory.name());
assertThat(actualDirectory.getLongName()).isEqualTo(directory.longName());
assertThat(actualDirectory.getPath()).isEqualTo(directory.path());
+ assertThat(actualDirectory.hasBranch()).isFalse();
+ assertThat(actualDirectory.hasPullRequest()).isFalse();
Component actualFile = componentByKey.get(file.getKey());
assertThat(actualFile.getQualifier()).isEqualTo(file.qualifier());
assertThat(actualFile.getName()).isEqualTo(file.name());
assertThat(actualFile.getLongName()).isEqualTo(file.longName());
assertThat(actualFile.getPath()).isEqualTo(file.path());
+ assertThat(actualFile.hasBranch()).isFalse();
+ assertThat(actualFile.hasPullRequest()).isFalse();
+ }
+
+ @Test
+ public void returns_branch_field_of_components_of_branch() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ ComponentDto branch = dbTester.components().insertProjectBranch(project);
+ userSessionRule.registerComponents(project, branch);
+ indexPermissions();
+ ComponentDto directory = dbTester.components().insertComponent(newDirectory(branch, "donut/acme"));
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(branch));
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ IssueDto fileHotspot = insertHotspot(branch, file, rule);
+ IssueDto dirHotspot = insertHotspot(branch, directory, rule);
+ IssueDto projectHotspot = insertHotspot(branch, branch, rule);
+ indexIssues();
+
+ SearchWsResponse response = newRequest(branch)
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(response.getHotspotsList())
+ .extracting(SearchWsResponse.Hotspot::getKey)
+ .containsOnly(fileHotspot.getKey(), dirHotspot.getKey(), projectHotspot.getKey());
+ assertThat(response.getComponentsList())
+ .extracting(Component::getKey)
+ .containsOnly(project.getKey(), directory.getKey(), file.getKey());
+ Map<String, Component> componentByKey = response.getComponentsList().stream().collect(uniqueIndex(Component::getKey));
+ Component actualProject = componentByKey.get(project.getKey());
+ assertThat(actualProject.getBranch()).isEqualTo(branch.getBranch());
+ assertThat(actualProject.hasPullRequest()).isFalse();
+ Component actualDirectory = componentByKey.get(directory.getKey());
+ assertThat(actualDirectory.getBranch()).isEqualTo(branch.getBranch());
+ assertThat(actualDirectory.hasPullRequest()).isFalse();
+ Component actualFile = componentByKey.get(file.getKey());
+ assertThat(actualFile.getBranch()).isEqualTo(branch.getBranch());
+ assertThat(actualFile.hasPullRequest()).isFalse();
+ }
+
+ @Test
+ public void returns_pullRequest_field_of_components_of_pullRequest() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ ComponentDto pullRequest = dbTester.components().insertProjectBranch(project, t -> t.setBranchType(BranchType.PULL_REQUEST));
+ userSessionRule.registerComponents(project, pullRequest);
+ indexPermissions();
+ ComponentDto directory = dbTester.components().insertComponent(newDirectory(pullRequest, "donut/acme"));
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(pullRequest));
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ IssueDto fileHotspot = insertHotspot(pullRequest, file, rule);
+ IssueDto dirHotspot = insertHotspot(pullRequest, directory, rule);
+ IssueDto projectHotspot = insertHotspot(pullRequest, pullRequest, rule);
+ indexIssues();
+
+ SearchWsResponse response = newRequest(pullRequest)
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(response.getHotspotsList())
+ .extracting(SearchWsResponse.Hotspot::getKey)
+ .containsOnly(fileHotspot.getKey(), dirHotspot.getKey(), projectHotspot.getKey());
+ assertThat(response.getComponentsList())
+ .extracting(Component::getKey)
+ .containsOnly(project.getKey(), directory.getKey(), file.getKey());
+ Map<String, Component> componentByKey = response.getComponentsList().stream().collect(uniqueIndex(Component::getKey));
+ Component actualProject = componentByKey.get(project.getKey());
+ assertThat(actualProject.hasBranch()).isFalse();
+ assertThat(actualProject.getPullRequest()).isEqualTo(pullRequest.getPullRequest());
+ Component actualDirectory = componentByKey.get(directory.getKey());
+ assertThat(actualDirectory.hasBranch()).isFalse();
+ assertThat(actualDirectory.getPullRequest()).isEqualTo(pullRequest.getPullRequest());
+ Component actualFile = componentByKey.get(file.getKey());
+ assertThat(actualFile.hasBranch()).isFalse();
+ assertThat(actualFile.getPullRequest()).isEqualTo(pullRequest.getPullRequest());
}
@Test
private TestRequest newRequest(ComponentDto project, @Nullable String status, @Nullable String resolution) {
TestRequest res = actionTester.newRequest()
.setParam("projectKey", project.getKey());
+ String branch = project.getBranch();
+ if (branch != null) {
+ res.setParam("branch", branch);
+ }
+ String pullRequest = project.getPullRequest();
+ if (pullRequest != null) {
+ res.setParam("pullRequest", pullRequest);
+ }
if (status != null) {
res.setParam("status", status);
}