@@ -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(); | |||
} | |||
} |
@@ -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()])); |
@@ -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,80 +51,149 @@ 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); | |||
@@ -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(); |
@@ -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); | |||
} | |||
} |
@@ -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()); | |||
} | |||
@@ -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; |
@@ -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 |
@@ -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"); | |||
@@ -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, |
@@ -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 |
@@ -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"); | |||
} | |||
} |
@@ -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); | |||
} |