Browse Source

SONAR-12717 add WS api/workflows/search

returns unresolved hotspots of a project, with components and rules details
tags/8.2.0.32929
Sébastien Lesaint 4 years ago
parent
commit
8d12ef5dc1

+ 43
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWs.java View File

@@ -0,0 +1,43 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.hotspot.ws;

import org.sonar.api.server.ws.WebService;

public class HotspotsWs implements WebService {

private final HotspotsWsAction[] actions;

public HotspotsWs(HotspotsWsAction... actions) {
this.actions = actions;
}

@Override
public void define(Context context) {
NewController controller = context.createController("api/hotspots");
controller.setDescription("Read and update Security Hotspots.");
controller.setSince("8.1");
for (HotspotsWsAction action : actions) {
action.define(controller);
}
controller.done();
}

}

+ 26
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsAction.java View File

@@ -0,0 +1,26 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.hotspot.ws;

import org.sonar.server.ws.WsAction;

public interface HotspotsWsAction extends WsAction {
// marker interface
}

+ 32
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java View File

@@ -0,0 +1,32 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.hotspot.ws;

import org.sonar.core.platform.Module;

public class HotspotsWsModule extends Module {
@Override
protected void configureModule() {
add(
SearchAction.class,
HotspotsWs.class
);
}
}

+ 297
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java View File

@@ -0,0 +1,297 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.hotspot.ws;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.search.SearchHit;
import org.sonar.api.resources.Language;
import org.sonar.api.resources.Languages;
import org.sonar.api.resources.Qualifiers;
import org.sonar.api.resources.Scopes;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.RuleType;
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.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;
import org.sonar.db.issue.IssueDto;
import org.sonar.db.rule.RuleDefinitionDto;
import org.sonar.server.es.SearchOptions;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.issue.index.IssueIndex;
import org.sonar.server.issue.index.IssueQuery;
import org.sonar.server.organization.DefaultOrganizationProvider;
import org.sonar.server.user.UserSession;
import org.sonarqube.ws.Common;
import org.sonarqube.ws.Hotspots;

import static com.google.common.base.Strings.nullToEmpty;
import static java.util.Optional.ofNullable;
import static org.sonar.api.server.ws.WebService.Param.PAGE;
import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
import static org.sonar.api.utils.DateUtils.formatDateTime;
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
import static org.sonar.server.ws.WsUtils.writeProtobuf;

public class SearchAction implements HotspotsWsAction {
private static final String PARAM_PROJECT_KEY = "projectKey";

private final DbClient dbClient;
private final UserSession userSession;
private final IssueIndex issueIndex;
private final DefaultOrganizationProvider defaultOrganizationProvider;
private final Languages languages;

public SearchAction(DbClient dbClient, UserSession userSession, IssueIndex issueIndex, DefaultOrganizationProvider defaultOrganizationProvider, Languages languages) {
this.dbClient = dbClient;
this.userSession = userSession;
this.issueIndex = issueIndex;
this.defaultOrganizationProvider = defaultOrganizationProvider;
this.languages = languages;
}

@Override
public void define(WebService.NewController controller) {

WebService.NewAction action = controller
.createAction("search")
.setHandler(this)
.setDescription("Search for Security Hotpots.")
.setSince("8.1")
.setInternal(true);

action.addPagingParams(100);
action.createParam(PARAM_PROJECT_KEY)
.setDescription("Key of the project")
.setExampleValue(KEY_PROJECT_EXAMPLE_001)
.setRequired(true);
// FIXME add response example and test it
// action.setResponseExample()
}

@Override
public void handle(Request request, Response response) throws Exception {
try (DbSession dbSession = dbClient.openSession(false)) {
String projectKey = request.mandatoryParam(PARAM_PROJECT_KEY);
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(String.format("Project '%s' not found", projectKey)));
userSession.checkComponentPermission(UserRole.USER, project);

List<IssueDto> orderedIssues = searchHotspots(request, dbSession, project);
SearchResponseData searchResponseData = new SearchResponseData(orderedIssues);
loadComponents(dbSession, searchResponseData);
loadRules(dbSession, searchResponseData);
writeProtobuf(formatResponse(searchResponseData), request, response);
}
}

private List<IssueDto> searchHotspots(Request request, DbSession dbSession, ComponentDto project) {
List<String> issueKeys = searchHotspots(request, project);

List<IssueDto> unorderedIssues = dbClient.issueDao().selectByKeys(dbSession, issueKeys);
Map<String, IssueDto> unorderedIssuesMap = unorderedIssues
.stream()
.collect(uniqueIndex(IssueDto::getKey, unorderedIssues.size()));

return issueKeys.stream()
.map(unorderedIssuesMap::get)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}

private List<String> searchHotspots(Request request, ComponentDto project) {
IssueQuery.Builder builder = IssueQuery.builder()
.projectUuids(Collections.singletonList(project.uuid()))
.organizationUuid(project.getOrganizationUuid())
.types(Collections.singleton(RuleType.SECURITY_HOTSPOT.name()))
.resolved(false);
IssueQuery query = builder.build();
SearchOptions searchOptions = new SearchOptions()
.setPage(request.mandatoryParamAsInt(PAGE), request.mandatoryParamAsInt(PAGE_SIZE));
SearchResponse result = issueIndex.search(query, searchOptions);
return Arrays.stream(result.getHits().getHits())
.map(SearchHit::getId)
.collect(MoreCollectors.toList(result.getHits().getHits().length));
}

private void loadComponents(DbSession dbSession, SearchResponseData searchResponseData) {
Set<String> componentKeys = searchResponseData.getOrderedHotspots()
.stream()
.flatMap(hotspot -> Stream.of(hotspot.getComponentKey(), hotspot.getProjectKey()))
.collect(Collectors.toSet());
if (!componentKeys.isEmpty()) {
searchResponseData.addComponents(dbClient.componentDao().selectByDbKeys(dbSession, componentKeys));
}
}

private void loadRules(DbSession dbSession, SearchResponseData searchResponseData) {
Set<RuleKey> ruleKeys = searchResponseData.getOrderedHotspots()
.stream()
.map(IssueDto::getRuleKey)
.collect(Collectors.toSet());
if (!ruleKeys.isEmpty()) {
searchResponseData.addRules(dbClient.ruleDao().selectDefinitionByKeys(dbSession, ruleKeys));
}
}

private Hotspots.SearchWsResponse formatResponse(SearchResponseData searchResponseData) {
Hotspots.SearchWsResponse.Builder responseBuilder = Hotspots.SearchWsResponse.newBuilder();
if (!searchResponseData.isEmpty()) {
formatHotspots(searchResponseData, responseBuilder);
formatComponents(searchResponseData, responseBuilder);
formatRules(searchResponseData, responseBuilder);
}
return responseBuilder.build();
}

private static void formatHotspots(SearchResponseData searchResponseData, Hotspots.SearchWsResponse.Builder responseBuilder) {
List<IssueDto> orderedHotspots = searchResponseData.getOrderedHotspots();
if (orderedHotspots.isEmpty()) {
return;
}

Hotspots.Hotspot.Builder builder = Hotspots.Hotspot.newBuilder();
for (IssueDto hotspot : orderedHotspots) {
builder
.clear()
.setKey(hotspot.getKey())
.setComponent(hotspot.getComponentKey())
.setProject(hotspot.getProjectKey())
.setRule(hotspot.getRuleKey().toString());
ofNullable(hotspot.getStatus()).ifPresent(builder::setStatus);
// FIXME resolution field will be added later
// ofNullable(hotspot.getResolution()).ifPresent(builder::setResolution);
ofNullable(hotspot.getLine()).ifPresent(builder::setLine);
builder.setMessage(nullToEmpty(hotspot.getMessage()));
ofNullable(hotspot.getAssigneeUuid()).ifPresent(builder::setAssignee);
// FIXME Filter author only if user is member of the organization (as done in issues/search WS)
// if (data.getUserOrganizationUuids().contains(component.getOrganizationUuid())) {
builder.setAuthor(nullToEmpty(hotspot.getAuthorLogin()));
// }
builder.setCreationDate(formatDateTime(hotspot.getIssueCreationDate()));
builder.setUpdateDate(formatDateTime(hotspot.getIssueUpdateDate()));

responseBuilder.addHotspots(builder.build());
}
}

private void formatComponents(SearchResponseData searchResponseData, Hotspots.SearchWsResponse.Builder responseBuilder) {
Set<ComponentDto> components = searchResponseData.getComponents();
if (components.isEmpty()) {
return;
}

Hotspots.Component.Builder builder = Hotspots.Component.newBuilder();
for (ComponentDto component : components) {
builder
.clear()
.setOrganization(defaultOrganizationProvider.get().getKey())
.setKey(component.getKey())
.setQualifier(component.qualifier())
.setName(component.name())
.setLongName(component.longName());
ofNullable(component.path()).ifPresent(builder::setPath);

responseBuilder.addComponents(builder.build());
}
}

private void formatRules(SearchResponseData searchResponseData, Hotspots.SearchWsResponse.Builder responseBuilder) {
Set<RuleDefinitionDto> rules = searchResponseData.getRules();
if (rules.isEmpty()) {
return;
}

Common.Rules.Builder rulesBuilder = Common.Rules.newBuilder();
Common.Rule.Builder ruleBuilder = Common.Rule.newBuilder();
for (RuleDefinitionDto rule : rules) {
ruleBuilder
.clear()
.setKey(rule.getKey().toString())
.setName(nullToEmpty(rule.getName()))
.setStatus(Common.RuleStatus.valueOf(rule.getStatus().name()));

String language = rule.getLanguage();
if (language == null) {
ruleBuilder.setLang("");
} else {
ruleBuilder.setLang(language);
Language lang = languages.get(language);
if (lang != null) {
ruleBuilder.setLangName(lang.getName());
}
}
rulesBuilder.addRules(ruleBuilder.build());
}
responseBuilder.setRules(rulesBuilder.build());
}

private static final class SearchResponseData {
private final List<IssueDto> orderedHotspots;
private final Set<ComponentDto> components = new HashSet<>();
private final Set<RuleDefinitionDto> rules = new HashSet<>();

private SearchResponseData(List<IssueDto> orderedHotspots) {
this.orderedHotspots = orderedHotspots;
}

boolean isEmpty() {
return orderedHotspots.isEmpty();
}

List<IssueDto> getOrderedHotspots() {
return orderedHotspots;
}

void addComponents(Collection<ComponentDto> components) {
this.components.addAll(components);
}

Set<ComponentDto> getComponents() {
return components;
}

void addRules(Collection<RuleDefinitionDto> rules) {
this.rules.addAll(rules);
}

Set<RuleDefinitionDto> getRules() {
return rules;
}
}
}

+ 23
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/package-info.java View File

@@ -0,0 +1,23 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.
*/
@ParametersAreNonnullByDefault
package org.sonar.server.hotspot.ws;

import javax.annotation.ParametersAreNonnullByDefault;

+ 36
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java View File

@@ -0,0 +1,36 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.hotspot.ws;

import org.junit.Test;
import org.sonar.core.platform.ComponentContainer;

import static org.assertj.core.api.Assertions.assertThat;
import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER;

public class HotspotsWsModuleTest {
@Test
public void verify_count_of_added_components() {
ComponentContainer container = new ComponentContainer();
new HotspotsWsModule().configure(container);
assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 2);
}

}

+ 63
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsTest.java View File

@@ -0,0 +1,63 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.hotspot.ws;

import java.util.Arrays;
import java.util.Random;
import java.util.stream.IntStream;
import org.junit.Test;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;

import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
import static org.assertj.core.api.Assertions.assertThat;

public class HotspotsWsTest {

@Test
public void define_controller() {
String[] actionKeys = IntStream.range(0, 1 + new Random().nextInt(12))
.mapToObj(i -> i + randomAlphanumeric(10))
.toArray(String[]::new);
HotspotsWsAction[] actions = Arrays.stream(actionKeys)
.map(actionKey -> new HotspotsWsAction() {
@Override
public void define(WebService.NewController context) {
context.createAction(actionKey).setHandler(this);
}

@Override
public void handle(Request request, Response response) {

}
})
.toArray(HotspotsWsAction[]::new);
WebService.Context context = new WebService.Context();
new HotspotsWs(actions).define(context);
WebService.Controller controller = context.controller("api/hotspots");

assertThat(controller).isNotNull();
assertThat(controller.description()).isNotEmpty();
assertThat(controller.since()).isEqualTo("8.1");
assertThat(controller.actions()).extracting(WebService.Action::key).containsOnly(actionKeys);
}

}

+ 494
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/SearchActionTest.java View File

@@ -0,0 +1,494 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.hotspot.ws;

import java.util.Arrays;
import java.util.Map;
import java.util.Random;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.issue.Issue;
import org.sonar.api.resources.AbstractLanguage;
import org.sonar.api.resources.Languages;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.RuleType;
import org.sonar.api.utils.System2;
import org.sonar.api.web.UserRole;
import org.sonar.db.DbClient;
import org.sonar.db.DbTester;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ComponentTesting;
import org.sonar.db.issue.IssueDto;
import org.sonar.db.rule.RuleDefinitionDto;
import org.sonar.db.rule.RuleTesting;
import org.sonar.server.es.EsTester;
import org.sonar.server.es.StartupIndexer;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.issue.index.IssueIndex;
import org.sonar.server.issue.index.IssueIndexer;
import org.sonar.server.issue.index.IssueIteratorFactory;
import org.sonar.server.organization.TestDefaultOrganizationProvider;
import org.sonar.server.permission.index.PermissionIndexer;
import org.sonar.server.permission.index.WebAuthorizationTypeSupport;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.TestRequest;
import org.sonar.server.ws.WsActionTester;
import org.sonarqube.ws.Common;
import org.sonarqube.ws.Common.RuleStatus;
import org.sonarqube.ws.Hotspots;
import org.sonarqube.ws.Hotspots.Component;
import org.sonarqube.ws.Hotspots.SearchWsResponse;

import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
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.rules.RuleType.SECURITY_HOTSPOT;
import static org.sonar.api.utils.DateUtils.formatDateTime;
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
import static org.sonar.db.component.ComponentTesting.newDirectory;
import static org.sonar.db.component.ComponentTesting.newFileDto;

public class SearchActionTest {
private static final Random RANDOM = new Random();

@Rule
public DbTester dbTester = DbTester.create(System2.INSTANCE);
@Rule
public EsTester es = EsTester.create();
@Rule
public UserSessionRule userSessionRule = UserSessionRule.standalone();

private DbClient dbClient = dbTester.getDbClient();
private TestDefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(dbTester);

private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSessionRule, new WebAuthorizationTypeSupport(userSessionRule));
private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient));
private StartupIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer);

private Languages languages = mock(Languages.class);
private SearchAction underTest = new SearchAction(dbClient, userSessionRule, issueIndex, defaultOrganizationProvider, languages);
private WsActionTester actionTester = new WsActionTester(underTest);

@Test
public void ws_is_internal() {
assertThat(actionTester.getDef().isInternal()).isTrue();
}

@Test
public void fails_with_IAE_if_parameter_projectKey_is_missing() {
TestRequest request = actionTester.newRequest();

assertThatThrownBy(request::execute)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("The 'projectKey' parameter is missing");
}

@Test
public void fails_with_NotFoundException_if_project_does_not_exist() {
String key = randomAlphabetic(12);
TestRequest request = actionTester.newRequest()
.setParam("projectKey", key);

assertThatThrownBy(request::execute)
.isInstanceOf(NotFoundException.class)
.hasMessage("Project '%s' not found", key);
}

@Test
public void fails_with_NotFoundException_if_project_is_not_a_project() {
ComponentDto project = dbTester.components().insertPrivateProject();
ComponentDto directory = dbTester.components().insertComponent(ComponentTesting.newDirectory(project, "foo"));
ComponentDto file = dbTester.components().insertComponent(ComponentTesting.newFileDto(project));
ComponentDto portfolio = dbTester.components().insertPrivatePortfolio(dbTester.getDefaultOrganization());
ComponentDto application = dbTester.components().insertPrivateApplication(dbTester.getDefaultOrganization());
TestRequest request = actionTester.newRequest();

for (ComponentDto component : Arrays.asList(directory, file, portfolio, application)) {
request.setParam("projectKey", component.getKey());

assertThatThrownBy(request::execute)
.isInstanceOf(NotFoundException.class)
.hasMessage("Project '%s' not found", component.getKey());
}
}

@Test
public void fails_with_ForbiddenException_if_project_is_private_and_not_allowed() {
ComponentDto project = dbTester.components().insertPrivateProject();
userSessionRule.registerComponents(project);
TestRequest request = actionTester.newRequest()
.setParam("projectKey", project.getKey());

assertThatThrownBy(request::execute)
.isInstanceOf(ForbiddenException.class)
.hasMessage("Insufficient privileges");
}

@Test
public void succeeds_on_public_project() {
ComponentDto project = dbTester.components().insertPublicProject();
userSessionRule.registerComponents(project);

SearchWsResponse response = actionTester.newRequest()
.setParam("projectKey", project.getKey())
.executeProtobuf(SearchWsResponse.class);

assertThat(response.getHotspotsList()).isEmpty();
assertThat(response.getComponentsList()).isEmpty();
assertThat(response.getRules().getRulesList()).isEmpty();
}

@Test
public void succeeds_on_private_project_with_permission() {
ComponentDto project = dbTester.components().insertPrivateProject();
userSessionRule.registerComponents(project);
userSessionRule.logIn().addProjectPermission(UserRole.USER, project);

SearchWsResponse response = actionTester.newRequest()
.setParam("projectKey", project.getKey())
.executeProtobuf(SearchWsResponse.class);

assertThat(response.getHotspotsList()).isEmpty();
assertThat(response.getComponentsList()).isEmpty();
assertThat(response.getRules().getRulesList()).isEmpty();
}

@Test
public void returns_no_hotspot_component_nor_rule_when_project_has_no_hotspot() {
ComponentDto project = dbTester.components().insertPublicProject();
userSessionRule.registerComponents(project);
indexPermissions();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
Arrays.stream(RuleType.values())
.filter(t -> t != SECURITY_HOTSPOT)
.forEach(ruleType -> {
RuleDefinitionDto rule = newRule(ruleType);
dbTester.issues().insert(rule, project, file, t -> t.setType(ruleType));
});
indexIssues();

SearchWsResponse response = actionTester.newRequest()
.setParam("projectKey", project.getKey())
.executeProtobuf(SearchWsResponse.class);

assertThat(response.getHotspotsList()).isEmpty();
assertThat(response.getComponentsList()).isEmpty();
assertThat(response.getRules().getRulesList()).isEmpty();
}

@Test
public void returns_hotspot_component_and_rule_when_project_has_hotspots() {
ComponentDto project = dbTester.components().insertPublicProject();
userSessionRule.registerComponents(project);
indexPermissions();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
ComponentDto fileWithHotspot = dbTester.components().insertComponent(newFileDto(project));
Arrays.stream(RuleType.values())
.filter(t -> t != SECURITY_HOTSPOT)
.forEach(ruleType -> {
RuleDefinitionDto rule = newRule(ruleType);
dbTester.issues().insert(rule, project, file, t -> t.setType(ruleType));
});
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));
})
.toArray(IssueDto[]::new);
indexIssues();

SearchWsResponse response = actionTester.newRequest()
.setParam("projectKey", project.getKey())
.executeProtobuf(SearchWsResponse.class);

assertThat(response.getHotspotsList())
.extracting(Hotspots.Hotspot::getKey)
.containsOnly(Arrays.stream(hotspots).map(IssueDto::getKey).toArray(String[]::new));
assertThat(response.getComponentsList())
.extracting(Component::getKey)
.containsOnly(project.getKey(), fileWithHotspot.getKey());
assertThat(response.getRules().getRulesList())
.extracting(Common.Rule::getKey)
.containsOnly(Arrays.stream(hotspots).map(t -> t.getRuleKey().toString()).toArray(String[]::new));
}

@Test
public void returns_hotspots_of_specified_project() {
ComponentDto project1 = dbTester.components().insertPublicProject();
ComponentDto project2 = dbTester.components().insertPublicProject();
userSessionRule.registerComponents(project1, project2);
indexPermissions();
ComponentDto file1 = dbTester.components().insertComponent(newFileDto(project1));
ComponentDto file2 = dbTester.components().insertComponent(newFileDto(project2));
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));
})
.toArray(IssueDto[]::new);
indexIssues();

SearchWsResponse responseProject1 = actionTester.newRequest()
.setParam("projectKey", project1.getKey())
.executeProtobuf(SearchWsResponse.class);

assertThat(responseProject1.getHotspotsList())
.extracting(Hotspots.Hotspot::getKey)
.doesNotContainAnyElementsOf(Arrays.stream(hotspots2).map(IssueDto::getKey).collect(Collectors.toList()));
assertThat(responseProject1.getComponentsList())
.extracting(Component::getKey)
.containsOnly(project1.getKey(), file1.getKey());
assertThat(responseProject1.getRules().getRulesList())
.extracting(Common.Rule::getKey)
.containsOnly(Arrays.stream(hotspots2).map(t -> t.getRuleKey().toString()).toArray(String[]::new));

SearchWsResponse responseProject2 = actionTester.newRequest()
.setParam("projectKey", project2.getKey())
.executeProtobuf(SearchWsResponse.class);

assertThat(responseProject2.getHotspotsList())
.extracting(Hotspots.Hotspot::getKey)
.containsOnly(Arrays.stream(hotspots2).map(IssueDto::getKey).toArray(String[]::new));
assertThat(responseProject2.getComponentsList())
.extracting(Component::getKey)
.containsOnly(project2.getKey(), file2.getKey());
assertThat(responseProject2.getRules().getRulesList())
.extracting(Common.Rule::getKey)
.containsOnly(Arrays.stream(hotspots2).map(t -> t.getRuleKey().toString()).toArray(String[]::new));
}

@Test
public void returns_only_unresolved_hotspots() {
ComponentDto project = dbTester.components().insertPublicProject();
userSessionRule.registerComponents(project);
indexPermissions();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
IssueDto unresolvedHotspot = dbTester.issues().insert(rule, project, file,
t -> t.setType(SECURITY_HOTSPOT).setResolution(null));
// unrealistic case since a resolution must be set, but shows a limit of current implementation
IssueDto badlyClosedHotspot = dbTester.issues().insert(rule, project, file,
t -> t.setType(SECURITY_HOTSPOT).setStatus(Issue.STATUS_CLOSED).setResolution(null));
IssueDto resolvedHotspot = dbTester.issues().insert(rule, project, file,
t -> t.setType(SECURITY_HOTSPOT).setResolution(randomAlphabetic(5)));
indexIssues();

SearchWsResponse response = actionTester.newRequest()
.setParam("projectKey", project.getKey())
.executeProtobuf(SearchWsResponse.class);

assertThat(response.getHotspotsList())
.extracting(Hotspots.Hotspot::getKey)
.containsOnly(unresolvedHotspot.getKey(), badlyClosedHotspot.getKey());
}

@Test
public void returns_fields_of_hotspot() {
ComponentDto project = dbTester.components().insertPublicProject();
userSessionRule.registerComponents(project);
indexPermissions();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
IssueDto hotspot = dbTester.issues().insert(rule, project, file,
t -> t.setType(SECURITY_HOTSPOT)
.setStatus(randomAlphabetic(11))
.setLine(RANDOM.nextInt(230))
.setMessage(randomAlphabetic(10))
.setAssigneeUuid(randomAlphabetic(9))
.setAuthorLogin(randomAlphabetic(8)));
indexIssues();

SearchWsResponse response = actionTester.newRequest()
.setParam("projectKey", project.getKey())
.executeProtobuf(SearchWsResponse.class);

assertThat(response.getHotspotsList()).hasSize(1);
Hotspots.Hotspot actual = response.getHotspots(0);
assertThat(actual.getComponent()).isEqualTo(file.getKey());
assertThat(actual.getProject()).isEqualTo(project.getKey());
assertThat(actual.getRule()).isEqualTo(hotspot.getRuleKey().toString());
assertThat(actual.getStatus()).isEqualTo(hotspot.getStatus());
// FIXME resolution field will be added later
// assertThat(actual.getResolution()).isEqualTo(hotspot.getResolution());
assertThat(actual.getLine()).isEqualTo(hotspot.getLine());
assertThat(actual.getMessage()).isEqualTo(hotspot.getMessage());
assertThat(actual.getAssignee()).isEqualTo(hotspot.getAssigneeUuid());
assertThat(actual.getAuthor()).isEqualTo(hotspot.getAuthorLogin());
assertThat(actual.getCreationDate()).isEqualTo(formatDateTime(hotspot.getIssueCreationDate()));
assertThat(actual.getUpdateDate()).isEqualTo(formatDateTime(hotspot.getIssueUpdateDate()));
}

@Test
public void does_not_fail_when_hotspot_has_none_of_the_nullable_fields() {
ComponentDto project = dbTester.components().insertPublicProject();
userSessionRule.registerComponents(project);
indexPermissions();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
dbTester.issues().insert(rule, project, file,
t -> t.setType(SECURITY_HOTSPOT)
.setStatus(null)
// FIXME resolution field will be added later
// .setResolution(null)
.setLine(null)
.setMessage(null)
.setAssigneeUuid(null)
.setAuthorLogin(null));
indexIssues();

SearchWsResponse response = actionTester.newRequest()
.setParam("projectKey", project.getKey())
.executeProtobuf(SearchWsResponse.class);

assertThat(response.getHotspotsList())
.hasSize(1);
Hotspots.Hotspot actual = response.getHotspots(0);
assertThat(actual.hasStatus()).isFalse();
// FIXME resolution field will be added later
// assertThat(actual.hasResolution()).isFalse();
assertThat(actual.hasLine()).isFalse();
assertThat(actual.getMessage()).isEmpty();
assertThat(actual.hasAssignee()).isFalse();
assertThat(actual.getAuthor()).isEmpty();
}

@Test
public void returns_details_of_components() {
ComponentDto project = dbTester.components().insertPublicProject();
userSessionRule.registerComponents(project);
indexPermissions();
ComponentDto directory = dbTester.components().insertComponent(newDirectory(project, "donut/acme"));
ComponentDto directory2 = dbTester.components().insertComponent(newDirectory(project, "foo/bar"));
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));
indexIssues();

SearchWsResponse response = actionTester.newRequest()
.setParam("projectKey", project.getKey())
.executeProtobuf(SearchWsResponse.class);

assertThat(response.getHotspotsList())
.extracting(Hotspots.Hotspot::getKey)
.containsOnly(fileHotspot.getKey(), dirHotspot.getKey(), projectHotspot.getKey());
assertThat(response.getComponentsList()).hasSize(3);
assertThat(response.getComponentsList())
.extracting(Component::getOrganization)
.containsOnly(defaultOrganizationProvider.get().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.getQualifier()).isEqualTo(project.qualifier());
assertThat(actualProject.getName()).isEqualTo(project.name());
assertThat(actualProject.getLongName()).isEqualTo(project.longName());
assertThat(actualProject.hasPath()).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());
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());
}

@Test
public void returns_details_of_rule_with_language_name_when_available() {
ComponentDto project = dbTester.components().insertPublicProject();
userSessionRule.registerComponents(project);
indexPermissions();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
String language1 = randomAlphabetic(3);
String language2 = randomAlphabetic(2);
RuleDefinitionDto rule1a = newRule(SECURITY_HOTSPOT, t -> t.setLanguage(language1));
RuleDefinitionDto rule1b = newRule(SECURITY_HOTSPOT, t -> t.setLanguage(language1));
RuleDefinitionDto rule2 = newRule(SECURITY_HOTSPOT, t -> t.setLanguage(language2));
when(languages.get(language1)).thenReturn(new AbstractLanguage(language1, language1 + "_name") {
@Override
public String[] getFileSuffixes() {
return new String[0];
}
});
IssueDto hotspot1a = dbTester.issues().insert(rule1a, project, file, t -> t.setType(SECURITY_HOTSPOT));
IssueDto hotspot1b = dbTester.issues().insert(rule1b, project, file, t -> t.setType(SECURITY_HOTSPOT));
IssueDto hotspot2 = dbTester.issues().insert(rule2, project, file, t -> t.setType(SECURITY_HOTSPOT));
indexIssues();

SearchWsResponse response = actionTester.newRequest()
.setParam("projectKey", project.getKey())
.executeProtobuf(SearchWsResponse.class);

assertThat(response.getHotspotsList()).hasSize(3);
assertThat(response.getRules().getRulesList()).hasSize(3);
Map<RuleKey, Common.Rule> rulesByKey = response.getRules().getRulesList()
.stream()
.collect(uniqueIndex(t -> RuleKey.parse(t.getKey())));
Common.Rule actualRule1a = rulesByKey.get(hotspot1a.getRuleKey());
assertThat(actualRule1a.getName()).isEqualTo(rule1a.getName());
assertThat(actualRule1a.getLang()).isEqualTo(rule1a.getLanguage());
assertThat(actualRule1a.getLangName()).isEqualTo(rule1a.getLanguage() + "_name");
assertThat(actualRule1a.getStatus()).isEqualTo(RuleStatus.valueOf(rule1a.getStatus().name()));
Common.Rule actualRule1b = rulesByKey.get(hotspot1b.getRuleKey());
assertThat(actualRule1b.getName()).isEqualTo(rule1b.getName());
assertThat(actualRule1b.getLang()).isEqualTo(rule1b.getLanguage());
assertThat(actualRule1b.getLangName()).isEqualTo(rule1b.getLanguage() + "_name");
assertThat(actualRule1b.getStatus()).isEqualTo(RuleStatus.valueOf(rule1b.getStatus().name()));
Common.Rule actualRule2 = rulesByKey.get(hotspot2.getRuleKey());
assertThat(actualRule2.getName()).isEqualTo(rule2.getName());
assertThat(actualRule2.getLang()).isEqualTo(rule2.getLanguage());
assertThat(actualRule2.hasLangName()).isFalse();
assertThat(actualRule2.getStatus()).isEqualTo(RuleStatus.valueOf(rule2.getStatus().name()));
}

private void indexPermissions() {
permissionIndexer.indexOnStartup(permissionIndexer.getIndexTypes());
}

private void indexIssues() {
issueIndexer.indexOnStartup(issueIndexer.getIndexTypes());
}

private RuleDefinitionDto newRule(RuleType ruleType) {
return newRule(ruleType, t -> {
});
}

private RuleDefinitionDto newRule(RuleType ruleType, Consumer<RuleDefinitionDto> populate) {
RuleDefinitionDto ruleDefinition = RuleTesting.newRule()
.setType(ruleType);
populate.accept(ruleDefinition);
dbTester.rules().insert(ruleDefinition);
return ruleDefinition;
}
}

+ 4
- 0
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java View File

@@ -74,6 +74,7 @@ import org.sonar.server.extension.CoreExtensionStopper;
import org.sonar.server.favorite.FavoriteModule;
import org.sonar.server.favorite.ws.FavoriteWsModule;
import org.sonar.server.health.NodeHealthModule;
import org.sonar.server.hotspot.ws.HotspotsWsModule;
import org.sonar.server.issue.AddTagsAction;
import org.sonar.server.issue.AssignAction;
import org.sonar.server.issue.CommentAction;
@@ -429,6 +430,9 @@ public class PlatformLevel4 extends PlatformLevel {
RemoveTagsAction.class,
IssueChangePostProcessorImpl.class,

// hotspots
HotspotsWsModule.class,

// source
SourceWsModule.class,


+ 61
- 0
sonar-ws/src/main/protobuf/ws-hotspots.proto View File

@@ -0,0 +1,61 @@
// SonarQube, open source software quality management tool.
// Copyright (C) 2008-2016 SonarSource
// mailto:contact AT sonarsource DOT com
//
// SonarQube 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.
//
// SonarQube 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.

syntax = "proto2";

package sonarqube.ws.hotspots;

import "ws-commons.proto";

option java_package = "org.sonarqube.ws";
option java_outer_classname = "Hotspots";
option optimize_for = SPEED;

// Response of GET api/hotspots/search
message SearchWsResponse {
optional sonarqube.ws.commons.Paging paging = 1;
repeated Hotspot hotspots = 2;
repeated Component components = 3;
optional sonarqube.ws.commons.Rules rules = 4;
}

message Hotspot {
optional string key = 1;
optional string component = 2;
optional string project = 3;
optional string rule = 4;
optional string status = 5;
// FIXME resolution field will be added later
// optional string resolution = 6;
optional int32 line = 7;
optional string message = 8;
optional string assignee = 9;
// SCM login of the committer who introduced the issue
optional string author = 10;
optional string creationDate = 11;
optional string updateDate = 12;
}

message Component {
optional string organization = 1;
optional string key = 2;
optional string qualifier = 3;
optional string name = 4;
optional string longName = 5;
optional string path = 6;
}

Loading…
Cancel
Save