diff options
12 files changed, 316 insertions, 105 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkDeleteAction.java b/server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkDeleteAction.java index d7da8b0dc6f..1b756893732 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkDeleteAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkDeleteAction.java @@ -19,23 +19,35 @@ */ package org.sonar.server.project.ws; -import java.util.List; -import java.util.Optional; -import javax.annotation.Nullable; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; +import org.sonar.api.server.ws.WebService.Param; import org.sonar.db.DbClient; import org.sonar.db.DbSession; -import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentQuery; import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.permission.OrganizationPermission; import org.sonar.server.component.ComponentCleanerService; +import org.sonar.server.project.Visibility; import org.sonar.server.user.UserSession; - -import static org.sonar.db.permission.OrganizationPermission.ADMINISTER; +import org.sonarqube.ws.client.project.SearchWsRequest; + +import static org.sonar.api.resources.Qualifiers.APP; +import static org.sonar.api.resources.Qualifiers.PROJECT; +import static org.sonar.api.resources.Qualifiers.VIEW; +import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01; +import static org.sonar.core.util.Uuids.UUID_EXAMPLE_02; +import static org.sonar.server.project.ws.SearchAction.buildDbQuery; import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; +import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_002; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ANALYZED_BEFORE; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ON_PROVISIONED_ONLY; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ORGANIZATION; import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECTS; import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECT_IDS; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_QUALIFIERS; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_VISIBILITY; public class BulkDeleteAction implements ProjectsWsAction { @@ -64,59 +76,79 @@ public class BulkDeleteAction implements ProjectsWsAction { .setSince("5.2") .setHandler(this); - action - .createParam(PARAM_PROJECT_IDS) - .setDescription("List of project IDs to delete") - .setDeprecatedKey("ids", "6.4") - .setDeprecatedSince("6.4") - .setExampleValue("ce4c03d6-430f-40a9-b777-ad877c00aa4d,c526ef20-131b-4486-9357-063fa64b5079"); + support.addOrganizationParam(action); action .createParam(PARAM_PROJECTS) - .setDescription("List of project keys to delete") + .setDescription("Comma-separated list of project keys") .setDeprecatedKey("keys", "6.4") - .setExampleValue(KEY_PROJECT_EXAMPLE_001); + .setExampleValue(String.join(",", KEY_PROJECT_EXAMPLE_001, KEY_PROJECT_EXAMPLE_002)); - support.addOrganizationParam(action); + action + .createParam(PARAM_PROJECT_IDS) + .setDescription("Comma-separated list of project ids") + .setDeprecatedKey("ids", "6.4") + .setDeprecatedSince("6.4") + .setExampleValue(String.join(",", UUID_EXAMPLE_01, UUID_EXAMPLE_02)); + + action.createParam(Param.TEXT_QUERY) + .setDescription("Limit to: <ul>" + + "<li>component names that contain the supplied string</li>" + + "<li>component keys that contain the supplied string</li>" + + "</ul>") + .setExampleValue("sonar"); + + action.createParam(PARAM_QUALIFIERS) + .setDescription("Comma-separated list of component qualifiers. Filter the results with the specified qualifiers") + .setPossibleValues(PROJECT, VIEW, APP) + .setDefaultValue(PROJECT); + + action.createParam(PARAM_VISIBILITY) + .setDescription("Filter the projects that should be visible to everyone (%s), or only specific user/groups (%s).<br/>" + + "If no visibility is specified, the default project visibility of the organization will be used.", + Visibility.PUBLIC.getLabel(), Visibility.PRIVATE.getLabel()) + .setRequired(false) + .setInternal(true) + .setSince("6.4") + .setPossibleValues(Visibility.getLabels()); + + action.createParam(PARAM_ANALYZED_BEFORE) + .setDescription("Filter the projects for which last analysis is older than the given date (exclusive).<br> " + + "Format: date or datetime ISO formats.") + .setSince("6.6"); + + action.createParam(PARAM_ON_PROVISIONED_ONLY) + .setDescription("Filter the projects that are provisioned") + .setBooleanPossibleValues() + .setDefaultValue("false") + .setSince("6.6"); } @Override public void handle(Request request, Response response) throws Exception { + SearchWsRequest searchRequest = toSearchWsRequest(request); userSession.checkLoggedIn(); - - List<String> uuids = request.paramAsStrings(PARAM_PROJECT_IDS); - List<String> keys = request.paramAsStrings(PARAM_PROJECTS); - String orgKey = request.param(ProjectsWsSupport.PARAM_ORGANIZATION); - try (DbSession dbSession = dbClient.openSession(false)) { - Optional<OrganizationDto> org = loadOrganizationByKey(dbSession, orgKey); - List<ComponentDto> projects = searchProjects(dbSession, uuids, keys); - projects.stream() - .filter(p -> !org.isPresent() || org.get().getUuid().equals(p.getOrganizationUuid())) + OrganizationDto organization = support.getOrganization(dbSession, searchRequest.getOrganization()); + userSession.checkPermission(OrganizationPermission.ADMINISTER, organization); + + ComponentQuery query = buildDbQuery(searchRequest); + dbClient.componentDao().selectByQuery(dbSession, organization.getUuid(), query, 0, Integer.MAX_VALUE) .forEach(p -> componentCleanerService.delete(dbSession, p)); } - response.noContent(); } - private Optional<OrganizationDto> loadOrganizationByKey(DbSession dbSession, @Nullable String orgKey) { - if (orgKey == null) { - userSession.checkIsSystemAdministrator(); - return Optional.empty(); - } - OrganizationDto org = support.getOrganization(dbSession, orgKey); - userSession.checkPermission(ADMINISTER, org); - return Optional.of(org); - } - - private List<ComponentDto> searchProjects(DbSession dbSession, @Nullable List<String> uuids, @Nullable List<String> keys) { - if (uuids != null) { - return dbClient.componentDao().selectByUuids(dbSession, uuids); - } - if (keys != null) { - return dbClient.componentDao().selectByKeys(dbSession, keys); - } - - throw new IllegalArgumentException("ids or keys must be provided"); + private static SearchWsRequest toSearchWsRequest(Request request) { + return SearchWsRequest.builder() + .setOrganization(request.param(PARAM_ORGANIZATION)) + .setQualifiers(request.mandatoryParamAsStrings(PARAM_QUALIFIERS)) + .setQuery(request.param(Param.TEXT_QUERY)) + .setVisibility(request.param(PARAM_VISIBILITY)) + .setAnalyzedBefore(request.param(PARAM_ANALYZED_BEFORE)) + .setOnProvisionedOnly(request.mandatoryParamAsBoolean(PARAM_ON_PROVISIONED_ONLY)) + .setProjects(request.paramAsStrings(PARAM_PROJECTS)) + .setProjectIds(request.paramAsStrings(PARAM_PROJECT_IDS)) + .build(); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/project/ws/SearchAction.java b/server/sonar-server/src/main/java/org/sonar/server/project/ws/SearchAction.java index 7cfd3cb87ee..1be8cde92c1 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/project/ws/SearchAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/project/ws/SearchAction.java @@ -176,7 +176,7 @@ public class SearchAction implements ProjectsWsAction { } } - private static ComponentQuery buildDbQuery(SearchWsRequest request) { + static ComponentQuery buildDbQuery(SearchWsRequest request) { List<String> qualifiers = request.getQualifiers(); ComponentQuery.Builder query = ComponentQuery.builder() .setQualifiers(qualifiers.toArray(new String[qualifiers.size()])); diff --git a/server/sonar-server/src/test/java/org/sonar/server/project/ws/BulkDeleteActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/project/ws/BulkDeleteActionTest.java index 5982f45f5b0..79f3806677d 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/project/ws/BulkDeleteActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/project/ws/BulkDeleteActionTest.java @@ -19,11 +19,17 @@ */ package org.sonar.server.project.ws; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.Date; +import java.util.stream.IntStream; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.server.ws.WebService.Param; import org.sonar.api.utils.System2; import org.sonar.db.DbClient; import org.sonar.db.DbSession; @@ -36,7 +42,8 @@ import org.sonar.server.exceptions.UnauthorizedException; import org.sonar.server.organization.BillingValidationsProxy; import org.sonar.server.organization.TestDefaultOrganizationProvider; import org.sonar.server.tester.UserSessionRule; -import org.sonar.server.ws.WsTester; +import org.sonar.server.ws.TestResponse; +import org.sonar.server.ws.WsActionTester; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Matchers.any; @@ -44,81 +51,150 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; +import static org.sonar.api.utils.DateUtils.formatDate; +import static org.sonar.db.component.SnapshotTesting.newAnalysis; import static org.sonar.db.permission.OrganizationPermission.ADMINISTER; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ANALYZED_BEFORE; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ON_PROVISIONED_ONLY; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ORGANIZATION; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECTS; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_QUALIFIERS; public class BulkDeleteActionTest { - private static final String ACTION = "bulk_delete"; - @Rule public DbTester db = DbTester.create(System2.INSTANCE); - @Rule public UserSessionRule userSession = UserSessionRule.standalone(); - @Rule public ExpectedException expectedException = ExpectedException.none(); private ComponentCleanerService componentCleanerService = mock(ComponentCleanerService.class); - private WsTester ws; private DbClient dbClient = db.getDbClient(); private ProjectsWsSupport support = new ProjectsWsSupport(dbClient, TestDefaultOrganizationProvider.from(db), mock(BillingValidationsProxy.class)); + private BulkDeleteAction underTest = new BulkDeleteAction(componentCleanerService, dbClient, userSession, support); + private WsActionTester ws = new WsActionTester(underTest); + private OrganizationDto org1; private OrganizationDto org2; @Before public void setUp() { - ws = new WsTester(new ProjectsWs(underTest)); org1 = db.organizations().insert(); org2 = db.organizations().insert(); } @Test - public void system_administrator_deletes_projects_by_uuids_in_all_organizations() throws Exception { - userSession.logIn().setSystemAdministrator(); + public void delete_projects_in_default_organization_if_no_org_provided() throws Exception { + userSession.logIn().setRoot(); + OrganizationDto defaultOrganization = db.getDefaultOrganization(); ComponentDto toDeleteInOrg1 = db.components().insertPrivateProject(org1); - ComponentDto toDeleteInOrg2 = db.components().insertPrivateProject(org2); - ComponentDto toKeep = db.components().insertPrivateProject(org2); + ComponentDto toDeleteInOrg2 = db.components().insertPrivateProject(defaultOrganization); + ComponentDto toKeep = db.components().insertPrivateProject(defaultOrganization); - WsTester.Result result = ws.newPostRequest("api/projects", ACTION) - .setParam("ids", toDeleteInOrg1.uuid() + "," + toDeleteInOrg2.uuid()) + TestResponse result = ws.newRequest() + .setParam("projectIds", toDeleteInOrg1.uuid() + "," + toDeleteInOrg2.uuid()) .execute(); - result.assertNoContent(); - verifyDeleted(toDeleteInOrg1, toDeleteInOrg2); + assertThat(result.getStatus()).isEqualTo(HttpURLConnection.HTTP_NO_CONTENT); + assertThat(result.getInput()).isEmpty(); + verifyDeleted(toDeleteInOrg2); } @Test - public void system_administrator_deletes_projects_by_keys_in_all_organizations() throws Exception { - userSession.logIn().setSystemAdministrator(); + public void delete_projects_by_keys() throws Exception { + userSession.logIn().setRoot(); ComponentDto toDeleteInOrg1 = db.components().insertPrivateProject(org1); - ComponentDto toDeleteInOrg2 = db.components().insertPrivateProject(org2); - ComponentDto toKeep = db.components().insertPrivateProject(org2); + ComponentDto toDeleteInOrg2 = db.components().insertPrivateProject(org1); + ComponentDto toKeep = db.components().insertPrivateProject(org1); - WsTester.Result result = ws.newPostRequest("api/projects", ACTION) - .setParam("keys", toDeleteInOrg1.getDbKey() + "," + toDeleteInOrg2.getDbKey()) + ws.newRequest() + .setParam(PARAM_ORGANIZATION, org1.getKey()) + .setParam(PARAM_PROJECTS, toDeleteInOrg1.getDbKey() + "," + toDeleteInOrg2.getDbKey()) .execute(); - result.assertNoContent(); verifyDeleted(toDeleteInOrg1, toDeleteInOrg2); } @Test public void projects_that_dont_exist_are_ignored_and_dont_break_bulk_deletion() throws Exception { - userSession.logIn().setSystemAdministrator(); + userSession.logIn().setRoot(); ComponentDto toDelete1 = db.components().insertPrivateProject(org1); ComponentDto toDelete2 = db.components().insertPrivateProject(org1); - WsTester.Result result = ws.newPostRequest("api/projects", ACTION) - .setParam("keys", toDelete1.getDbKey() + ",missing," + toDelete2.getDbKey() + ",doesNotExist") + ws.newRequest() + .setParam("organization", org1.getKey()) + .setParam("projects", toDelete1.getDbKey() + ",missing," + toDelete2.getDbKey() + ",doesNotExist") .execute(); - result.assertNoContent(); verifyDeleted(toDelete1, toDelete2); } @Test + public void old_projects() { + userSession.logIn().addPermission(ADMINISTER, db.getDefaultOrganization()); + long aLongTimeAgo = 1_000_000_000L; + long recentTime = 3_000_000_000L; + ComponentDto oldProject = db.components().insertPublicProject(); + db.getDbClient().snapshotDao().insert(db.getSession(), newAnalysis(oldProject).setCreatedAt(aLongTimeAgo)); + ComponentDto recentProject = db.components().insertPublicProject(); + db.getDbClient().snapshotDao().insert(db.getSession(), newAnalysis(recentProject).setCreatedAt(recentTime)); + db.commit(); + + ws.newRequest() + .setParam(PARAM_ANALYZED_BEFORE, formatDate(new Date(recentTime))) + .execute(); + + verifyDeleted(oldProject); + } + + @Test + public void provisioned_projects() { + userSession.logIn().addPermission(ADMINISTER, db.getDefaultOrganization()); + ComponentDto provisionedProject = db.components().insertPrivateProject(); + ComponentDto analyzedProject = db.components().insertPrivateProject(); + db.components().insertSnapshot(newAnalysis(analyzedProject)); + + ws.newRequest().setParam(PARAM_ON_PROVISIONED_ONLY, "true").execute(); + + verifyDeleted(provisionedProject); + } + + @Test + public void delete_more_than_50_projects() { + userSession.logIn().addPermission(ADMINISTER, db.getDefaultOrganization()); + ComponentDto[] projects = IntStream.range(0, 55).mapToObj(i -> db.components().insertPrivateProject()).toArray(ComponentDto[]::new); + + ws.newRequest().execute(); + + verifyDeleted(projects); + } + + @Test + public void projects_and_views() throws IOException { + userSession.logIn().addPermission(ADMINISTER, db.getDefaultOrganization()); + ComponentDto project = db.components().insertPrivateProject(); + ComponentDto view = db.components().insertView(); + + ws.newRequest().setParam(PARAM_QUALIFIERS, String.join(",", Qualifiers.PROJECT, Qualifiers.VIEW)).execute(); + + verifyDeleted(project, view); + } + + @Test + public void delete_by_key_query_with_partial_match_case_insensitive() throws IOException { + userSession.logIn().addPermission(ADMINISTER, db.getDefaultOrganization()); + ComponentDto matchKeyProject = db.components().insertPrivateProject(p -> p.setDbKey("project-_%-key")); + ComponentDto matchUppercaseKeyProject = db.components().insertPrivateProject(p -> p.setDbKey("PROJECT-_%-KEY")); + ComponentDto noMatchProject = db.components().insertPrivateProject(p -> p.setDbKey("project-key-without-escaped-characters")); + + ws.newRequest().setParam(Param.TEXT_QUERY, "JeCt-_%-k").execute(); + + verifyDeleted(matchKeyProject, matchUppercaseKeyProject); + } + + @Test public void throw_ForbiddenException_if_organization_administrator_does_not_set_organization_parameter() throws Exception { userSession.logIn().addPermission(ADMINISTER, org1); ComponentDto project = db.components().insertPrivateProject(org1); @@ -126,8 +202,8 @@ public class BulkDeleteActionTest { expectedException.expect(ForbiddenException.class); expectedException.expectMessage("Insufficient privileges"); - ws.newPostRequest("api/projects", ACTION) - .setParam("keys", project.getDbKey()) + ws.newRequest() + .setParam("projects", project.getDbKey()) .execute(); verifyNoDeletions(); @@ -139,11 +215,10 @@ public class BulkDeleteActionTest { ComponentDto toDelete = db.components().insertPrivateProject(org1); ComponentDto cantBeDeleted = db.components().insertPrivateProject(org2); - WsTester.Result result = ws.newPostRequest("api/projects", ACTION) + ws.newRequest() .setParam("organization", org1.getKey()) - .setParam("keys", toDelete.getDbKey() + "," + cantBeDeleted.getDbKey()) + .setParam("projects", toDelete.getDbKey() + "," + cantBeDeleted.getDbKey()) .execute(); - result.assertNoContent(); verifyDeleted(toDelete); } @@ -153,7 +228,7 @@ public class BulkDeleteActionTest { expectedException.expect(UnauthorizedException.class); expectedException.expectMessage("Authentication is required"); - ws.newPostRequest("api/projects", ACTION) + ws.newRequest() .setParam("ids", "whatever-the-uuid").execute(); verifyNoDeletions(); @@ -166,7 +241,7 @@ public class BulkDeleteActionTest { expectedException.expect(ForbiddenException.class); expectedException.expectMessage("Insufficient privileges"); - ws.newPostRequest("api/projects", ACTION) + ws.newRequest() .setParam("ids", "whatever-the-uuid").execute(); verifyNoDeletions(); @@ -179,7 +254,7 @@ public class BulkDeleteActionTest { expectedException.expect(ForbiddenException.class); expectedException.expectMessage("Insufficient privileges"); - ws.newPostRequest("api/projects", ACTION) + ws.newRequest() .setParam("organization", org1.getKey()) .setParam("ids", "whatever-the-uuid") .execute(); diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/BaseService.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/BaseService.java index edfd8c7339b..9d5f9e9ce39 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/BaseService.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/BaseService.java @@ -19,7 +19,6 @@ */ package org.sonarqube.ws.client; -import com.google.common.base.Joiner; import com.google.protobuf.Message; import com.google.protobuf.Parser; import java.io.InputStream; @@ -34,8 +33,6 @@ import static com.google.common.base.Strings.isNullOrEmpty; public abstract class BaseService { - private static final Joiner MULTI_VALUES_JOINER = Joiner.on(","); - private final WsConnector wsConnector; protected final String controller; @@ -71,6 +68,6 @@ public abstract class BaseService { @CheckForNull protected static String inlineMultipleParamValue(@Nullable List<String> values) { - return values == null ? null : MULTI_VALUES_JOINER.join(values); + return values == null ? null : String.join(",", values); } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsService.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsService.java index 6ebe8424074..11dc0c4495f 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsService.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsService.java @@ -85,10 +85,15 @@ public class ProjectsService extends BaseService { .setParam("project", request.getKey())); } - public void bulkDelete(BulkDeleteRequest request) { + public void bulkDelete(SearchWsRequest request) { PostRequest post = new PostRequest(path("bulk_delete")) - .setParam("organization", request.getOrganization()) - .setParam("projects", String.join(",", request.getProjectKeys())); + .setParam(PARAM_ORGANIZATION, request.getOrganization()) + .setParam(PARAM_QUALIFIERS, inlineMultipleParamValue(request.getQualifiers())) + .setParam(PARAM_ANALYZED_BEFORE, request.getAnalyzedBefore()) + .setParam(TEXT_QUERY, request.getQuery()) + .setParam(PARAM_ON_PROVISIONED_ONLY, request.isOnProvisionedOnly()) + .setParam(PARAM_PROJECTS, inlineMultipleParamValue(request.getProjects())) + .setParam(PARAM_PROJECT_IDS, inlineMultipleParamValue(request.getProjectIds())); call(post); } @@ -121,8 +126,8 @@ public class ProjectsService extends BaseService { .setParam(PAGE, request.getPage()) .setParam(PAGE_SIZE, request.getPageSize()) .setParam(PARAM_ON_PROVISIONED_ONLY, request.isOnProvisionedOnly()) - .setParam(PARAM_PROJECTS, request.getProjects()) - .setParam(PARAM_PROJECT_IDS, request.getProjectIds()); + .setParam(PARAM_PROJECTS, inlineMultipleParamValue(request.getProjects())) + .setParam(PARAM_PROJECT_IDS, inlineMultipleParamValue(request.getProjectIds())); return call(get, SearchWsResponse.parser()); } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/project/SearchWsRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/project/SearchWsRequest.java index 422bb90f6d9..90544b37fc2 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/project/SearchWsRequest.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/project/SearchWsRequest.java @@ -19,12 +19,13 @@ */ package org.sonarqube.ws.client.project; -import java.util.ArrayList; import java.util.List; import javax.annotation.CheckForNull; import javax.annotation.Nullable; +import org.sonar.api.resources.Qualifiers; import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Collections.singletonList; import static java.util.Objects.requireNonNull; import static org.sonarqube.ws.client.project.ProjectsWsParameters.MAX_PAGE_SIZE; @@ -108,7 +109,7 @@ public class SearchWsRequest { public static class Builder { private String organization; - private List<String> qualifiers = new ArrayList<>(); + private List<String> qualifiers = singletonList(Qualifiers.PROJECT); private Integer page; private Integer pageSize; private String query; diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/project/ProjectsServiceTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/project/ProjectsServiceTest.java index f3727f38851..d1a094f3fcc 100644 --- a/sonar-ws/src/test/java/org/sonarqube/ws/client/project/ProjectsServiceTest.java +++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/project/ProjectsServiceTest.java @@ -125,11 +125,24 @@ public class ProjectsServiceTest { @Test public void bulk_delete() { - BulkDeleteRequest request = BulkDeleteRequest.builder().setProjectKeys(Arrays.asList("p1", "p2")).setOrganization("my-org").build(); - underTest.bulkDelete(request); + underTest.bulkDelete(SearchWsRequest.builder() + .setOrganization("default") + .setQuery("project") + .setQualifiers(asList("TRK", "VW")) + .setAnalyzedBefore("2017-09-01") + .setProjects(Arrays.asList("P1", "P2")) + .setOnProvisionedOnly(true) + .build()); - assertThat(serviceTester.getPostRequest().getPath()).isEqualTo("api/projects/bulk_delete"); - assertThat(serviceTester.getPostRequest().getParams()).containsOnly(entry("organization", "my-org"), entry("projects", "p1,p2")); + serviceTester.assertThat(serviceTester.getPostRequest()) + .hasPath("bulk_delete") + .hasParam("organization", "default") + .hasParam("q", "project") + .hasParam("analyzedBefore", "2017-09-01") + .hasParam("qualifiers", "TRK,VW") + .hasParam("onProvisionedOnly", "true") + .hasParam("projects", "P1,P2") + .andNoOtherParam(); } @Test diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/project/SearchWsRequestTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/project/SearchWsRequestTest.java index 121a6d5b075..bf0f455215d 100644 --- a/sonar-ws/src/test/java/org/sonarqube/ws/client/project/SearchWsRequestTest.java +++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/project/SearchWsRequestTest.java @@ -50,7 +50,7 @@ public class SearchWsRequestTest { } @Test - public void fail_when_page_size_is_greather_then_500() throws Exception { + public void fail_when_page_size_is_greater_then_500() throws Exception { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Page size must not be greater than 500"); diff --git a/tests/src/test/java/org/sonarqube/tests/Category6Suite.java b/tests/src/test/java/org/sonarqube/tests/Category6Suite.java index d075f209596..e1e4e666dc6 100644 --- a/tests/src/test/java/org/sonarqube/tests/Category6Suite.java +++ b/tests/src/test/java/org/sonarqube/tests/Category6Suite.java @@ -35,7 +35,7 @@ import org.sonarqube.tests.organization.OrganizationMembershipUiTest; import org.sonarqube.tests.organization.OrganizationTest; import org.sonarqube.tests.organization.PersonalOrganizationTest; import org.sonarqube.tests.organization.RootUserOnOrganizationTest; -import org.sonarqube.tests.projectAdministration.ProjectDeletionTest; +import org.sonarqube.tests.projectAdministration.ProjectDeleteTest; import org.sonarqube.tests.projectAdministration.ProjectKeyUpdateTest; import org.sonarqube.tests.projectAdministration.ProjectProvisioningTest; import org.sonarqube.tests.projectAdministration.ProjectSearchTest; @@ -77,7 +77,7 @@ import static util.ItUtils.xooPlugin; LeakProjectsPageTest.class, SearchProjectsTest.class, RulesWsTest.class, - ProjectDeletionTest.class, + ProjectDeleteTest.class, ProjectProvisioningTest.class, ProjectKeyUpdateTest.class, ProjectSearchTest.class, diff --git a/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectAdministrationTest.java b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectAdministrationTest.java index 3a4d1aad047..b001176c914 100644 --- a/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectAdministrationTest.java +++ b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectAdministrationTest.java @@ -47,8 +47,10 @@ import org.sonarqube.ws.WsPermissions; import org.sonarqube.ws.client.permission.AddUserToTemplateWsRequest; import org.sonarqube.ws.client.permission.CreateTemplateWsRequest; import org.sonarqube.ws.client.permission.UsersWsRequest; +import org.sonarqube.ws.client.project.SearchWsRequest; import static com.codeborne.selenide.Selenide.$; +import static java.util.Collections.singletonList; import static org.apache.commons.lang.time.DateUtils.addDays; import static org.assertj.core.api.Assertions.assertThat; import static util.ItUtils.getComponent; @@ -97,14 +99,15 @@ public class ProjectAdministrationTest { @Test public void fail_when_trying_to_delete_a_file() { - expectedException.expect(HttpException.class); scanSampleWithDate(ANALYSIS_DATE); - assertThat(getComponent(orchestrator, PROJECT_KEY)).isNotNull(); assertThat(getComponent(orchestrator, FILE_KEY)).isNotNull(); - // it's forbidden to delete only some files - orchestrator.getServer().adminWsClient().post(DELETE_WS_ENDPOINT, "keys", FILE_KEY); + expectedException.expect(org.sonarqube.ws.client.HttpException.class); + + tester.wsClient().projects().bulkDelete(SearchWsRequest.builder() + .setQualifiers(singletonList("FIL")) + .setProjects(singletonList(FILE_KEY)).build()); } @Test diff --git a/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectBulkDeleteTest.java b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectBulkDeleteTest.java new file mode 100644 index 00000000000..28d601e0e21 --- /dev/null +++ b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectBulkDeleteTest.java @@ -0,0 +1,86 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.sonarqube.tests.projectAdministration; + +import com.sonar.orchestrator.Orchestrator; +import java.util.List; +import java.util.stream.IntStream; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.sonarqube.tests.Category6Suite; +import org.sonarqube.tests.Tester; +import org.sonarqube.ws.Organizations; +import org.sonarqube.ws.WsProjects.CreateWsResponse; +import org.sonarqube.ws.WsProjects.SearchWsResponse.Component; +import org.sonarqube.ws.client.project.SearchWsRequest; + +import static org.assertj.core.api.Assertions.assertThat; +import static util.ItUtils.runProjectAnalysis; + +public class ProjectBulkDeleteTest { + + @ClassRule + public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR; + @Rule + public Tester tester = new Tester(orchestrator); + + @Test + public void delete_projects() { + Organizations.Organization organization = tester.organizations().generate(); + CreateWsResponse.Project firstProvisionedProject = tester.projects().generate(organization, p -> p.setKey("first-provisioned-project")); + CreateWsResponse.Project secondProvisionedProject = tester.projects().generate(organization, p -> p.setKey("second-provisioned-project")); + CreateWsResponse.Project analyzedProject = tester.projects().generate(organization); + + analyzeProject(analyzedProject.getKey(), organization.getKey()); + + tester.wsClient().projects().bulkDelete(SearchWsRequest.builder() + .setOrganization(organization.getKey()) + .setQuery("FIRST-PROVISIONED") + .setOnProvisionedOnly(true).build()); + + List<Component> projects = tester.wsClient().projects().search(SearchWsRequest.builder().setOrganization(organization.getKey()).build()).getComponentsList(); + assertThat(projects).extracting(Component::getKey) + .containsExactlyInAnyOrder(analyzedProject.getKey(), secondProvisionedProject.getKey()) + .doesNotContain(firstProvisionedProject.getKey()); + } + + @Test + public void delete_more_than_50_projects_at_the_same_time() { + Organizations.Organization organization = tester.organizations().generate(); + IntStream.range(0, 60).forEach(i -> tester.projects().generate(organization)); + SearchWsRequest request = SearchWsRequest.builder().setOrganization(organization.getKey()).build(); + assertThat(tester.wsClient().projects().search(request).getPaging().getTotal()).isEqualTo(60); + + tester.wsClient().projects().bulkDelete(request); + + assertThat(tester.wsClient().projects().search(request).getComponentsList()).isEmpty(); + assertThat(tester.wsClient().projects().search(request).getPaging().getTotal()).isEqualTo(0); + } + + private void analyzeProject(String projectKey, String organizationKey) { + runProjectAnalysis(orchestrator, "shared/xoo-sample", + "sonar.organization", organizationKey, + "sonar.projectKey", projectKey, + "sonar.login", "admin", + "sonar.password", "admin"); + } +} diff --git a/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectDeletionTest.java b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectDeleteTest.java index d9ea32b0bb9..b0b43103ac6 100644 --- a/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectDeletionTest.java +++ b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectDeleteTest.java @@ -39,7 +39,6 @@ import org.sonarqube.ws.WsProjects.CreateWsResponse.Project; import org.sonarqube.ws.client.GetRequest; import org.sonarqube.ws.client.WsResponse; import org.sonarqube.ws.client.component.SearchProjectsRequest; -import org.sonarqube.ws.client.project.BulkDeleteRequest; import org.sonarqube.ws.client.project.CreateRequest; import org.sonarqube.ws.client.project.DeleteRequest; import org.sonarqube.ws.client.project.SearchWsRequest; @@ -48,7 +47,7 @@ import util.ItUtils; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; -public class ProjectDeletionTest { +public class ProjectDeleteTest { @ClassRule public static final Orchestrator orchestrator = Category6Suite.ORCHESTRATOR; @@ -137,9 +136,9 @@ public class ProjectDeletionTest { } private void bulkDeleteProjects(Organizations.Organization organization, Project... projects) { - BulkDeleteRequest request = BulkDeleteRequest.builder() + SearchWsRequest request = SearchWsRequest.builder() .setOrganization(organization.getKey()) - .setProjectKeys(Arrays.stream(projects).map(Project::getKey).collect(Collectors.toList())) + .setProjects(Arrays.stream(projects).map(Project::getKey).collect(Collectors.toList())) .build(); tester.wsClient().projects().bulkDelete(request); } |