From 7d5898c9adcefa1f0b75b4e5d8f0a1a4331d1954 Mon Sep 17 00:00:00 2001 From: =?utf8?q?S=C3=A9bastien=20Lesaint?= Date: Tue, 17 Dec 2019 17:40:41 +0100 Subject: [PATCH] SONAR-12726 api/hotspots/search supports Applications --- .../sonar/db/component/ComponentDbTester.java | 15 ++ .../sonar/server/hotspot/ws/SearchAction.java | 21 ++- .../server/hotspot/ws/SearchActionTest.java | 139 +++++++++++++++++- 3 files changed, 163 insertions(+), 12 deletions(-) diff --git a/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/component/ComponentDbTester.java b/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/component/ComponentDbTester.java index 70099cb0a87..b6072571857 100644 --- a/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/component/ComponentDbTester.java +++ b/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/component/ComponentDbTester.java @@ -166,16 +166,31 @@ public class ComponentDbTester { return insertComponentImpl(ComponentTesting.newView(organization).setPrivate(false), false, dtoPopulators); } + @SafeVarargs + public final ComponentDto insertPrivatePortfolio(Consumer... dtoPopulators) { + return insertComponentImpl(ComponentTesting.newView(db.getDefaultOrganization()).setPrivate(true), true, dtoPopulators); + } + @SafeVarargs public final ComponentDto insertPrivatePortfolio(OrganizationDto organization, Consumer... dtoPopulators) { return insertComponentImpl(ComponentTesting.newView(organization).setPrivate(true), true, dtoPopulators); } + @SafeVarargs + public final ComponentDto insertPublicApplication(Consumer... dtoPopulators) { + return insertComponentImpl(ComponentTesting.newApplication(db.getDefaultOrganization()).setPrivate(false), false, dtoPopulators); + } + @SafeVarargs public final ComponentDto insertPublicApplication(OrganizationDto organization, Consumer... dtoPopulators) { return insertComponentImpl(ComponentTesting.newApplication(organization).setPrivate(false), false, dtoPopulators); } + @SafeVarargs + public final ComponentDto insertPrivateApplication(Consumer... dtoPopulators) { + return insertComponentImpl(ComponentTesting.newApplication(db.getDefaultOrganization()).setPrivate(true), true, dtoPopulators); + } + @SafeVarargs public final ComponentDto insertPrivateApplication(OrganizationDto organization, Consumer... dtoPopulators) { return insertComponentImpl(ComponentTesting.newApplication(organization).setPrivate(true), true, dtoPopulators); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java index 8609c927d79..d55096ccb7d 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java @@ -60,6 +60,7 @@ import org.sonarqube.ws.Common; import org.sonarqube.ws.Hotspots; import org.sonarqube.ws.Hotspots.SearchWsResponse; +import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Preconditions.checkArgument; import static java.lang.String.format; import static java.util.Collections.singleton; @@ -84,6 +85,7 @@ import static org.sonar.server.ws.WsUtils.writeProtobuf; import static org.sonarqube.ws.WsUtils.nullToEmpty; public class SearchAction implements HotspotsWsAction { + private static final Set SUPPORTED_QUALIFIERS = ImmutableSet.of(Qualifiers.PROJECT, Qualifiers.APP); private static final String PARAM_PROJECT_KEY = "projectKey"; private static final String PARAM_STATUS = "status"; private static final String PARAM_RESOLUTION = "resolution"; @@ -118,7 +120,7 @@ public class SearchAction implements HotspotsWsAction { action.addPagingParams(100); action.createParam(PARAM_PROJECT_KEY) .setDescription(format( - "Key of the project. This parameter is required unless %s is provided.", + "Key of the project or application. This parameter is required unless %s is provided.", PARAM_HOTSPOTS)) .setExampleValue(KEY_PROJECT_EXAMPLE_001); action.createParam(PARAM_BRANCH) @@ -162,7 +164,7 @@ public class SearchAction implements HotspotsWsAction { WsRequest wsRequest = toWsRequest(request); validateParameters(wsRequest); try (DbSession dbSession = dbClient.openSession(false)) { - Optional project = getAndValidateProject(dbSession, wsRequest); + Optional project = getAndValidateProjectOrApplication(dbSession, wsRequest); SearchResponseData searchResponseData = searchHotspots(wsRequest, dbSession, project, wsRequest.getHotspotKeys()); loadComponents(dbSession, searchResponseData); @@ -222,10 +224,10 @@ public class SearchAction implements HotspotsWsAction { } } - private Optional getAndValidateProject(DbSession dbSession, WsRequest wsRequest) { + private Optional getAndValidateProjectOrApplication(DbSession dbSession, WsRequest wsRequest) { return wsRequest.getProjectKey().map(projectKey -> { ComponentDto project = getProject(dbSession, projectKey, wsRequest.getBranch(), wsRequest.getPullRequest()) - .filter(t -> Scopes.PROJECT.equals(t.scope()) && Qualifiers.PROJECT.equals(t.qualifier())) + .filter(t -> Scopes.PROJECT.equals(t.scope()) && SUPPORTED_QUALIFIERS.contains(t.qualifier())) .filter(ComponentDto::isEnabled) .orElseThrow(() -> new NotFoundException(format("Project '%s' not found", projectKey))); userSession.checkComponentPermission(UserRole.USER, project); @@ -274,13 +276,20 @@ public class SearchAction implements HotspotsWsAction { project.ifPresent(p -> { builder.organizationUuid(p.getOrganizationUuid()); + String projectUuid = firstNonNull(p.getMainBranchProjectUuid(), p.uuid()); + if (Qualifiers.APP.equals(p.qualifier())) { + builder.viewUuids(singletonList(projectUuid)); + } else { + builder.projectUuids(singletonList(projectUuid)); + } + if (p.getMainBranchProjectUuid() == null) { builder.mainBranch(true); - builder.projectUuids(singletonList(p.uuid())); } else { - builder.branchUuid(p.projectUuid()); + builder.branchUuid(p.uuid()); builder.mainBranch(false); } + if (wsRequest.isSinceLeakPeriod()) { dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, p.uuid()) .map(s -> longToDate(s.getPeriodDate())) diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/SearchActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/SearchActionTest.java index 2e967221156..e61ee0238fa 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/SearchActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/SearchActionTest.java @@ -52,7 +52,6 @@ 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; @@ -64,6 +63,7 @@ import org.sonar.server.permission.index.WebAuthorizationTypeSupport; import org.sonar.server.security.SecurityStandards; import org.sonar.server.security.SecurityStandards.SQCategory; import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.view.index.ViewIndexer; import org.sonar.server.ws.TestRequest; import org.sonar.server.ws.WsActionTester; import org.sonarqube.ws.Hotspots; @@ -105,7 +105,8 @@ public class SearchActionTest { 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 ViewIndexer viewIndexer = new ViewIndexer(dbClient, es.client()); + private PermissionIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer); private HotspotWsResponseFormatter responseFormatter = new HotspotWsResponseFormatter(defaultOrganizationProvider); private SearchAction underTest = new SearchAction(dbClient, userSessionRule, issueIndex, responseFormatter); @@ -268,15 +269,14 @@ public class SearchActionTest { } @Test - public void fails_with_NotFoundException_if_project_is_not_a_project() { + public void fails_with_NotFoundException_if_project_is_neither_a_project_nor_an_application() { 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()); + ComponentDto portfolio = dbTester.components().insertPrivatePortfolio(); TestRequest request = actionTester.newRequest(); - for (ComponentDto component : Arrays.asList(directory, file, portfolio, application)) { + for (ComponentDto component : Arrays.asList(directory, file, portfolio)) { request.setParam("projectKey", component.getKey()); assertThatThrownBy(request::execute) @@ -296,6 +296,17 @@ public class SearchActionTest { .hasMessage("Insufficient privileges"); } + @Test + public void fails_with_ForbiddenException_if_application_is_private_and_not_allowed() { + ComponentDto application = dbTester.components().insertPrivateApplication(); + userSessionRule.registerComponents(application); + TestRequest request = newRequest(application); + + assertThatThrownBy(request::execute) + .isInstanceOf(ForbiddenException.class) + .hasMessage("Insufficient privileges"); + } + @Test public void succeeds_on_public_project() { ComponentDto project = dbTester.components().insertPublicProject(); @@ -308,6 +319,18 @@ public class SearchActionTest { assertThat(response.getComponentsList()).isEmpty(); } + @Test + public void succeeds_on_public_application() { + ComponentDto application = dbTester.components().insertPublicApplication(); + userSessionRule.registerComponents(application); + + SearchWsResponse response = newRequest(application) + .executeProtobuf(SearchWsResponse.class); + + assertThat(response.getHotspotsList()).isEmpty(); + assertThat(response.getComponentsList()).isEmpty(); + } + @Test public void succeeds_on_private_project_with_permission() { ComponentDto project = dbTester.components().insertPrivateProject(); @@ -321,6 +344,19 @@ public class SearchActionTest { assertThat(response.getComponentsList()).isEmpty(); } + @Test + public void succeeds_on_private_application_with_permission() { + ComponentDto application = dbTester.components().insertPrivateApplication(); + userSessionRule.registerComponents(application); + userSessionRule.logIn().addProjectPermission(UserRole.USER, application); + + SearchWsResponse response = newRequest(application) + .executeProtobuf(SearchWsResponse.class); + + assertThat(response.getHotspotsList()).isEmpty(); + assertThat(response.getComponentsList()).isEmpty(); + } + @Test public void does_not_fail_if_rule_of_hotspot_does_not_exist_in_DB() { ComponentDto project = dbTester.components().insertPublicProject(); @@ -462,6 +498,93 @@ public class SearchActionTest { .containsOnly(project2.getKey(), file2.getKey()); } + @Test + public void returns_hotspots_of_specified_application() { + ComponentDto application1 = dbTester.components().insertPublicApplication(); + ComponentDto application2 = dbTester.components().insertPublicApplication(); + ComponentDto project1 = dbTester.components().insertPublicProject(); + ComponentDto project2 = dbTester.components().insertPublicProject(); + dbTester.components().insertComponent(ComponentTesting.newProjectCopy(project1, application1)); + dbTester.components().insertComponent(ComponentTesting.newProjectCopy(project2, application2)); + indexViews(); + userSessionRule.registerComponents(application1, application2, 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); + insertHotspot(project1, file1, rule); + return insertHotspot(project2, file2, rule); + }) + .toArray(IssueDto[]::new); + indexIssues(); + + SearchWsResponse responseApplication1 = newRequest(application1) + .executeProtobuf(SearchWsResponse.class); + + assertThat(responseApplication1.getHotspotsList()) + .extracting(SearchWsResponse.Hotspot::getKey) + .doesNotContainAnyElementsOf(Arrays.stream(hotspots2).map(IssueDto::getKey).collect(toList())); + assertThat(responseApplication1.getComponentsList()) + .extracting(Component::getKey) + .containsOnly(project1.getKey(), file1.getKey()); + + SearchWsResponse responseApplication2 = newRequest(application2) + .executeProtobuf(SearchWsResponse.class); + + assertThat(responseApplication2.getHotspotsList()) + .extracting(SearchWsResponse.Hotspot::getKey) + .containsOnly(Arrays.stream(hotspots2).map(IssueDto::getKey).toArray(String[]::new)); + assertThat(responseApplication2.getComponentsList()) + .extracting(Component::getKey) + .containsOnly(project2.getKey(), file2.getKey()); + } + + @Test + public void returns_hotspots_of_specified_application_branch() { + ComponentDto application = dbTester.components().insertPublicApplication(); + ComponentDto applicationBranch = dbTester.components().insertProjectBranch(application); + ComponentDto project1 = dbTester.components().insertPublicProject(); + ComponentDto project2 = dbTester.components().insertPublicProject(); + dbTester.components().insertComponent(ComponentTesting.newProjectCopy(project1, application)); + dbTester.components().insertComponent(ComponentTesting.newProjectCopy(project2, applicationBranch)); + indexViews(); + userSessionRule.registerComponents(application, applicationBranch, 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); + insertHotspot(project1, file1, rule); + return insertHotspot(project2, file2, rule); + }) + .toArray(IssueDto[]::new); + indexIssues(); + + SearchWsResponse responseApplication = newRequest(application) + .executeProtobuf(SearchWsResponse.class); + + assertThat(responseApplication.getHotspotsList()) + .extracting(SearchWsResponse.Hotspot::getKey) + .doesNotContainAnyElementsOf(Arrays.stream(hotspots2).map(IssueDto::getKey).collect(toList())); + assertThat(responseApplication.getComponentsList()) + .extracting(Component::getKey) + .containsOnly(project1.getKey(), file1.getKey()); + + SearchWsResponse responseApplicationBranch = newRequest(applicationBranch) + .executeProtobuf(SearchWsResponse.class); + + assertThat(responseApplicationBranch.getHotspotsList()) + .extracting(SearchWsResponse.Hotspot::getKey) + .containsOnly(Arrays.stream(hotspots2).map(IssueDto::getKey).toArray(String[]::new)); + assertThat(responseApplicationBranch.getComponentsList()) + .extracting(Component::getKey) + .containsOnly(project2.getKey(), file2.getKey()); + + } + @Test public void returns_hotspot_of_branch_or_pullRequest() { ComponentDto project = dbTester.components().insertPublicProject(); @@ -1261,6 +1384,10 @@ public class SearchActionTest { issueIndexer.indexOnStartup(issueIndexer.getIndexTypes()); } + private void indexViews() { + viewIndexer.indexOnStartup(viewIndexer.getIndexTypes()); + } + private RuleDefinitionDto newRule(RuleType ruleType) { return newRule(ruleType, t -> { }); -- 2.39.5