*/
package org.sonar.server.hotspot.ws;
+import com.google.common.collect.ImmutableSet;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.utils.Paging;
import org.sonar.api.web.UserRole;
+import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentDto;
private static final String PARAM_PROJECT_KEY = "projectKey";
private static final String PARAM_STATUS = "status";
private static final String PARAM_RESOLUTION = "resolution";
+ private static final String PARAM_HOTSPOTS = "hotspots";
private final DbClient dbClient;
private final UserSession userSession;
action.addPagingParams(100);
action.createParam(PARAM_PROJECT_KEY)
- .setDescription("Key of the project")
- .setExampleValue(KEY_PROJECT_EXAMPLE_001)
- .setRequired(true);
+ .setDescription(format(
+ "Key of the project. This parameter is required unless %s is provided.",
+ PARAM_HOTSPOTS))
+ .setExampleValue(KEY_PROJECT_EXAMPLE_001);
+ action.createParam(PARAM_HOTSPOTS)
+ .setDescription(format(
+ "Comma-separated list of Security Hotspot keys. This parameter is required unless %s is provided.",
+ PARAM_PROJECT_KEY))
+ .setExampleValue(KEY_PROJECT_EXAMPLE_001);
action.createParam(PARAM_STATUS)
- .setDescription("If provided, only Security Hotspots with the specified status are returned.")
+ .setDescription("If '%s' is provided, only Security Hotspots with the specified status are returned.", PARAM_PROJECT_KEY)
.setPossibleValues(STATUS_TO_REVIEW, STATUS_REVIEWED)
.setRequired(false);
action.createParam(PARAM_RESOLUTION)
.setDescription(format(
- "If provided and if status is '%s', only Security Hotspots with the specified resolution are returned.",
- STATUS_REVIEWED))
+ "If '%s' is provided and if status is '%s', only Security Hotspots with the specified resolution are returned.",
+ PARAM_PROJECT_KEY, STATUS_REVIEWED))
.setPossibleValues(RESOLUTION_FIXED, RESOLUTION_SAFE)
.setRequired(false);
// FIXME add response example and test it
@Override
public void handle(Request request, Response response) throws Exception {
WsRequest wsRequest = toWsRequest(request);
+ validateParameters(wsRequest);
try (DbSession dbSession = dbClient.openSession(false)) {
- ComponentDto project = validateRequest(dbSession, wsRequest);
+ Optional<ComponentDto> project = getAndValidateProject(dbSession, wsRequest);
- SearchResponseData searchResponseData = searchHotspots(wsRequest, dbSession, project);
+ SearchResponseData searchResponseData = searchHotspots(wsRequest, dbSession, project, wsRequest.getHotspotKeys());
loadComponents(dbSession, searchResponseData);
loadRules(dbSession, searchResponseData);
writeProtobuf(formatResponse(searchResponseData), request, response);
}
private static WsRequest toWsRequest(Request request) {
+ List<String> hotspotKeysList = request.paramAsStrings(PARAM_HOTSPOTS);
+ 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.mandatoryParam(PARAM_PROJECT_KEY),
+ request.param(PARAM_PROJECT_KEY), hotspotKeys,
request.param(PARAM_STATUS), request.param(PARAM_RESOLUTION));
}
- private ComponentDto validateRequest(DbSession dbSession, WsRequest wsRequest) {
+ private static void validateParameters(WsRequest wsRequest) {
+ checkArgument(
+ wsRequest.getProjectKey().isPresent() || !wsRequest.getHotspotKeys().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(),
+ "Parameter '%s' can't be used with parameter '%s'", PARAM_STATUS, PARAM_HOTSPOTS);
+ checkArgument(!wsRequest.getResolution().isPresent() || wsRequest.getHotspotKeys().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(),
"Value '%s' of parameter '%s' can only be provided if value of parameter '%s' is '%s'",
- resolution, PARAM_RESOLUTION, PARAM_STATUS, STATUS_REVIEWED)
- );
-
- ComponentDto project = dbClient.componentDao().selectByKey(dbSession, wsRequest.getProjectKey())
- .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", wsRequest.getProjectKey())));
- userSession.checkComponentPermission(UserRole.USER, project);
- return project;
+ resolution, PARAM_RESOLUTION, PARAM_STATUS, STATUS_REVIEWED));
}
- private SearchResponseData searchHotspots(WsRequest wsRequest, DbSession dbSession, ComponentDto project) {
- SearchResponse result = doIndexSearch(wsRequest, project);
+ private Optional<ComponentDto> getAndValidateProject(DbSession dbSession, WsRequest wsRequest) {
+ return wsRequest.getProjectKey().map(projectKey -> {
+ ComponentDto project = dbClient.componentDao().selectByKey(dbSession, projectKey)
+ .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 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())
.map(SearchHit::getId)
.collect(toList(result.getHits().getHits().length));
.collect(Collectors.toList());
}
- private SearchResponse doIndexSearch(WsRequest wsRequest, ComponentDto project) {
+ private SearchResponse doIndexSearch(WsRequest wsRequest, Optional<ComponentDto> project, Set<String> hotspotKeys) {
IssueQuery.Builder builder = IssueQuery.builder()
- .projectUuids(Collections.singletonList(project.uuid()))
- .organizationUuid(project.getOrganizationUuid())
.types(singleton(RuleType.SECURITY_HOTSPOT.name()))
.sort(IssueQuery.SORT_HOTSPOTS)
.asc(true);
+ project.ifPresent(p -> {
+ builder.projectUuids(Collections.singletonList(p.uuid()));
+ builder.organizationUuid(p.getOrganizationUuid());
+ });
+ if (!hotspotKeys.isEmpty()) {
+ builder.issueKeys(hotspotKeys);
+ }
wsRequest.getStatus().ifPresent(status -> builder.resolved(STATUS_REVIEWED.equals(status)));
wsRequest.getResolution().ifPresent(resolution -> builder.resolutions(singleton(resolution)));
private final int page;
private final int index;
private final String projectKey;
+ private final Set<String> hotspotKeys;
private final String status;
private final String resolution;
- private WsRequest(int page, int index, String projectKey, @Nullable String status, @Nullable String resolution) {
+ private WsRequest(int page, int index, @Nullable String projectKey, Set<String> hotspotKeys, @Nullable String status, @Nullable String resolution) {
this.page = page;
this.index = index;
this.projectKey = projectKey;
+ this.hotspotKeys = hotspotKeys;
this.status = status;
this.resolution = resolution;
}
return index;
}
- String getProjectKey() {
- return projectKey;
+ Optional<String> getProjectKey() {
+ return ofNullable(projectKey);
+ }
+
+ Set<String> getHotspotKeys() {
+ return hotspotKeys;
}
Optional<String> getStatus() {
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.function.Consumer;
-import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.sonarqube.ws.Hotspots.SearchWsResponse;
import static java.util.Collections.singleton;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.assertj.core.api.Assertions.assertThat;
}
@Test
- public void fails_with_IAE_if_parameter_projectKey_is_missing() {
+ public void fails_with_IAE_if_parameters_projectKey_and_hotspots_are_missing() {
TestRequest request = actionTester.newRequest();
assertThatThrownBy(request::execute)
.isInstanceOf(IllegalArgumentException.class)
- .hasMessage("The 'projectKey' parameter is missing");
+ .hasMessage("A value must be provided for either parameter 'projectKey' or parameter 'hotspots'");
}
@Test
.toArray(Object[][]::new);
}
+ @Test
+ @UseDataProvider("validStatusesAndResolutions")
+ public void fail_with_IAE_if_parameter_status_is_specified_with_hotspots_parameter(String status, @Nullable String notUsed) {
+ TestRequest request = actionTester.newRequest()
+ .setParam("hotspots", randomAlphabetic(12))
+ .setParam("status", status);
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Parameter 'status' can't be used with parameter 'hotspots'");
+ }
+
@Test
@UseDataProvider("badResolutions")
public void fails_with_IAE_if_resolution_parameter_is_neither_FIXED_nor_SAFE(String badResolution) {
.hasMessage("Value '" + resolution + "' of parameter 'resolution' can only be provided if value of parameter 'status' is 'REVIEWED'");
}
+ @Test
+ @UseDataProvider("fixedOrSafeResolution")
+ public void fails_with_IAE_if_resolution_is_provided_with_hotspots_parameter(String resolution) {
+ TestRequest request = actionTester.newRequest()
+ .setParam("hotspots", randomAlphabetic(13))
+ .setParam("resolution", resolution);
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Parameter 'resolution' can't be used with parameter 'hotspots'");
+ }
+
@DataProvider
public static Object[][] fixedOrSafeResolution() {
return new Object[][] {
IssueDto[] hotspots = IntStream.range(0, 1 + RANDOM.nextInt(10))
.mapToObj(i -> {
RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
- return dbTester.issues().insert(rule, project, file, t -> t.setType(SECURITY_HOTSPOT));
+ return insertHotspot(project, file, rule);
})
.toArray(IssueDto[]::new);
indexIssues();
IssueDto[] hotspots = IntStream.range(0, 1 + RANDOM.nextInt(10))
.mapToObj(i -> {
RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
- return dbTester.issues().insert(rule, project, fileWithHotspot, t -> t.setType(SECURITY_HOTSPOT));
+ return insertHotspot(project, fileWithHotspot, rule);
})
.toArray(IssueDto[]::new);
indexIssues();
IssueDto[] hotspots = IntStream.range(0, 1 + RANDOM.nextInt(10))
.mapToObj(i -> {
RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
- return dbTester.issues().insert(rule, project, project, t -> t.setType(SECURITY_HOTSPOT));
+ return insertHotspot(project, project, rule);
})
.toArray(IssueDto[]::new);
indexIssues();
IssueDto[] hotspots2 = IntStream.range(0, 1 + RANDOM.nextInt(10))
.mapToObj(i -> {
RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
- dbTester.issues().insert(rule, project1, file1, t -> t.setType(SECURITY_HOTSPOT));
- return dbTester.issues().insert(rule, project2, file2, t -> t.setType(SECURITY_HOTSPOT));
+ insertHotspot(project1, file1, rule);
+ return insertHotspot(project2, file2, rule);
})
.toArray(IssueDto[]::new);
indexIssues();
assertThat(responseProject1.getHotspotsList())
.extracting(SearchWsResponse.Hotspot::getKey)
- .doesNotContainAnyElementsOf(Arrays.stream(hotspots2).map(IssueDto::getKey).collect(Collectors.toList()));
+ .doesNotContainAnyElementsOf(Arrays.stream(hotspots2).map(IssueDto::getKey).collect(toList()));
assertThat(responseProject1.getComponentsList())
.extracting(Component::getKey)
.containsOnly(project1.getKey(), file1.getKey());
indexPermissions();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
List<IssueDto> hotspots = insertRandomNumberOfHotspotsOfAllSupportedStatusesAndResolutions(project, file)
- .collect(Collectors.toList());
+ .collect(toList());
indexIssues();
SearchWsResponse response = newRequest(project, null, null)
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
List<IssueDto> reviewedHotspots = insertRandomNumberOfHotspotsOfAllSupportedStatusesAndResolutions(project, file)
.filter(t -> STATUS_REVIEWED.equals(t.getStatus()))
- .collect(Collectors.toList());
+ .collect(toList());
indexIssues();
SearchWsResponse response = newRequest(project, STATUS_REVIEWED, null)
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
List<IssueDto> safeHotspots = insertRandomNumberOfHotspotsOfAllSupportedStatusesAndResolutions(project, file)
.filter(t -> STATUS_REVIEWED.equals(t.getStatus()) && RESOLUTION_SAFE.equals(t.getResolution()))
- .collect(Collectors.toList());
+ .collect(toList());
indexIssues();
SearchWsResponse response = newRequest(project, STATUS_REVIEWED, RESOLUTION_SAFE)
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
List<IssueDto> fixedHotspots = insertRandomNumberOfHotspotsOfAllSupportedStatusesAndResolutions(project, file)
.filter(t -> STATUS_REVIEWED.equals(t.getStatus()) && RESOLUTION_FIXED.equals(t.getResolution()))
- .collect(Collectors.toList());
+ .collect(toList());
indexIssues();
SearchWsResponse response = newRequest(project, STATUS_REVIEWED, RESOLUTION_FIXED)
.setStatus(status)
.setResolution(resolution));
})
- .collect(Collectors.toList());
+ .collect(toList());
Collections.shuffle(hotspots);
hotspots.forEach(t -> dbTester.issues().insertIssue(t));
return hotspots.stream();
indexPermissions();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT, t -> t.setSecurityStandards(securityStandards));
- IssueDto hotspot = dbTester.issues().insert(rule, project, file, t -> t.setType(SECURITY_HOTSPOT));
+ IssueDto hotspot = insertHotspot(project, file, rule);
indexIssues();
SearchWsResponse response = newRequest(project)
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
ComponentDto file2 = dbTester.components().insertComponent(newFileDto(project));
RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
- IssueDto fileHotspot = dbTester.issues().insert(rule, project, file, t -> t.setType(SECURITY_HOTSPOT));
- IssueDto dirHotspot = dbTester.issues().insert(rule, project, directory, t -> t.setType(SECURITY_HOTSPOT));
- IssueDto projectHotspot = dbTester.issues().insert(rule, project, project, t -> t.setType(SECURITY_HOTSPOT));
+ IssueDto fileHotspot = insertHotspot(project, file, rule);
+ IssueDto dirHotspot = insertHotspot(project, directory, rule);
+ IssueDto projectHotspot = insertHotspot(project, project, rule);
indexIssues();
SearchWsResponse response = newRequest(project)
newIssue(rule1, project, file).setKee(sqCategory + "_a").setType(SECURITY_HOTSPOT),
newIssue(rule2, project, file).setKee(sqCategory + "_b").setType(SECURITY_HOTSPOT));
})
- .collect(Collectors.toList());
+ .collect(toList());
String[] expectedHotspotKeys = hotspots.stream().map(IssueDto::getKey).toArray(String[]::new);
// insert hotspots in random order
Collections.shuffle(hotspots);
newIssue(rule, project, file1).setType(SECURITY_HOTSPOT).setLine(11).setKee("b"),
newIssue(rule, project, file2).setType(SECURITY_HOTSPOT).setLine(null),
newIssue(rule, project, file2).setType(SECURITY_HOTSPOT).setLine(2))
- .collect(Collectors.toList());
+ .collect(toList());
String[] expectedHotspotKeys = hotspots.stream().map(IssueDto::getKey).toArray(String[]::new);
// insert hotspots in random order
Collections.shuffle(hotspots);
List<IssueDto> hotspots = IntStream.range(0, total)
.mapToObj(i -> newIssue(rule, project, file).setType(SECURITY_HOTSPOT).setLine(i))
.map(i -> dbTester.issues().insertIssue(i))
- .collect(Collectors.toList());
+ .collect(toList());
indexIssues();
TestRequest request = newRequest(project);
List<IssueDto> hotspots = IntStream.range(0, total)
.mapToObj(i -> newIssue(rule, project, file).setType(SECURITY_HOTSPOT).setLine(i).setKee("issue_" + i))
.map(i -> dbTester.issues().insertIssue(i))
- .collect(Collectors.toList());
+ .collect(toList());
indexIssues();
SearchWsResponse response = newRequest(project)
assertThat(response.getPaging().getPageSize()).isEqualTo(pageSize);
}
+ @Test
+ public void returns_empty_if_none_of_hotspot_keys_exist() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+ indexPermissions();
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ List<IssueDto> hotspots = IntStream.range(0, 1 + RANDOM.nextInt(15))
+ .mapToObj(i -> newIssue(rule, project, file).setType(SECURITY_HOTSPOT).setLine(i))
+ .map(i -> dbTester.issues().insertIssue(i))
+ .collect(toList());
+ indexIssues();
+
+ SearchWsResponse response = newRequest(IntStream.range(0, 1 + RANDOM.nextInt(30)).mapToObj(i -> "key_" + i).collect(toList()))
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(response.getHotspotsList()).isEmpty();
+ }
+
+ @Test
+ public void returns_specified_hotspots() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+ indexPermissions();
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+ int total = 1 + RANDOM.nextInt(20);
+ List<IssueDto> hotspots = IntStream.range(0, total)
+ .mapToObj(i -> newIssue(rule, project, file).setType(SECURITY_HOTSPOT).setLine(i))
+ .map(i -> dbTester.issues().insertIssue(i))
+ .collect(toList());
+ Collections.shuffle(hotspots);
+ List<IssueDto> selectedHotspots = hotspots.stream().limit(total == 1 ? 1 : 1 + RANDOM.nextInt(total - 1)).collect(toList());
+ indexIssues();
+
+ SearchWsResponse response = newRequest(selectedHotspots.stream().map(IssueDto::getKey).collect(toList()))
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(response.getHotspotsList())
+ .extracting(SearchWsResponse.Hotspot::getKey)
+ .containsExactlyInAnyOrder(selectedHotspots.stream().map(IssueDto::getKey).toArray(String[]::new));
+ }
+
+ private IssueDto insertHotspot(ComponentDto project, ComponentDto file, RuleDefinitionDto rule) {
+ return dbTester.issues().insert(rule, project, file, t -> t.setType(SECURITY_HOTSPOT));
+ }
+
private TestRequest newRequest(ComponentDto project) {
return newRequest(project, null, null);
}
return res;
}
+ private TestRequest newRequest(Collection<String> hotspotKeys) {
+ return actionTester.newRequest()
+ .setParam("hotspots", hotspotKeys.stream().collect(joining(",")));
+ }
+
private void indexPermissions() {
permissionIndexer.indexOnStartup(permissionIndexer.getIndexTypes());
}