From 48d5ec5040eccf6e7638170135426f247807a585 Mon Sep 17 00:00:00 2001 From: Julien Camus Date: Tue, 10 Dec 2024 16:00:22 +0100 Subject: SONAR-23845 Add pagination to SearchBitbucketServerReposAction --- .../bitbucketserver/BitbucketServerRestClient.java | 5 +- .../alm/client/bitbucketserver/RepositoryList.java | 43 ++- .../BitbucketServerRestClientTest.java | 57 ++- .../ListBitbucketServerProjectsActionIT.java | 26 ++ .../SearchBitbucketServerReposActionIT.java | 418 ++++++++++++++------- .../ListBitbucketServerProjectsAction.java | 7 +- .../SearchBitbucketServerReposAction.java | 49 ++- 7 files changed, 427 insertions(+), 178 deletions(-) (limited to 'server') diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClient.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClient.java index 83ec942df8b..ecd611535e6 100644 --- a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClient.java +++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClient.java @@ -84,10 +84,11 @@ public class BitbucketServerRestClient { doGet(personalAccessToken, url, body -> buildGson().fromJson(body, RepositoryList.class)); } - public RepositoryList getRepos(String serverUrl, String token, @Nullable String project, @Nullable String repo) { + public RepositoryList getRepos(String serverUrl, String token, @Nullable String project, @Nullable String repo, @Nullable Integer start, int pageSize) { String projectOrEmpty = Optional.ofNullable(project).orElse(""); String repoOrEmpty = Optional.ofNullable(repo).orElse(""); - HttpUrl url = buildUrl(serverUrl, format("/rest/api/1.0/repos?projectname=%s&name=%s", projectOrEmpty, repoOrEmpty)); + String startOrEmpty = Optional.ofNullable(start).map(String::valueOf).orElse(""); + HttpUrl url = buildUrl(serverUrl, format("/rest/api/1.0/repos?projectname=%s&name=%s&start=%s&limit=%s", projectOrEmpty, repoOrEmpty, startOrEmpty, pageSize)); return doGet(token, url, body -> buildGson().fromJson(body, RepositoryList.class)); } diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/RepositoryList.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/RepositoryList.java index 1fb522c4b46..7f0f14edc55 100644 --- a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/RepositoryList.java +++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/RepositoryList.java @@ -19,56 +19,59 @@ */ package org.sonar.alm.client.bitbucketserver; -import com.google.gson.Gson; import com.google.gson.annotations.SerializedName; -import java.util.ArrayList; import java.util.List; public class RepositoryList { @SerializedName("isLastPage") - private boolean isLastPage; + private boolean lastPage; + + @SerializedName("nextPageStart") + private int nextPageStart; + + @SerializedName("size") + private int size; @SerializedName("values") private List values; - public RepositoryList() { + private RepositoryList() { // http://stackoverflow.com/a/18645370/229031 - this(false, new ArrayList<>()); + this(true, 0, 0, List.of()); } - public RepositoryList(boolean isLastPage, List values) { - this.isLastPage = isLastPage; + public RepositoryList(boolean lastPage, int nextPageStart, int size, List values) { + this.lastPage = lastPage; + this.nextPageStart = nextPageStart; + this.size = size; this.values = values; } - static RepositoryList parse(String json) { - return new Gson().fromJson(json, RepositoryList.class); + public boolean isLastPage() { + return lastPage; } - public boolean isLastPage() { - return isLastPage; + public int getNextPageStart() { + return nextPageStart; } - public RepositoryList setLastPage(boolean lastPage) { - isLastPage = lastPage; - return this; + public int getSize() { + return size; } public List getValues() { return values; } - public RepositoryList setValues(List values) { + public void setValues(List values) { this.values = values; - return this; } @Override public String toString() { - return "{" + - "isLastPage=" + isLastPage + - ", values=" + values + - '}'; + return "{isLastPage=%s, nextPageStart=%s, size=%s, values=%s}" + .formatted(lastPage, nextPageStart, size, values); } + } diff --git a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClientTest.java b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClientTest.java index dc719609ab1..52176ae199b 100644 --- a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClientTest.java +++ b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClientTest.java @@ -123,7 +123,7 @@ public class BitbucketServerRestClientTest { } """)); - RepositoryList gsonBBSRepoList = underTest.getRepos(server.url("/").toString(), "token", "", ""); + RepositoryList gsonBBSRepoList = underTest.getRepos(server.url("/").toString(), "token", "", "", null, 25); assertThat(gsonBBSRepoList.isLastPage()).isTrue(); assertThat(gsonBBSRepoList.getValues()).hasSize(2); assertThat(gsonBBSRepoList.getValues()).extracting(Repository::getId, Repository::getName, Repository::getSlug, @@ -700,6 +700,61 @@ public class BitbucketServerRestClientTest { assertThat(BitbucketServerRestClient.equals(MediaType.parse("application/Json"), MediaType.parse("application/JSON"))).isTrue(); } + @Test + @UseDataProvider("validStartAndPageSizeProvider") + public void get_repositories_with_pagination(int start, int pageSize, boolean isLastPage, int totalRepos) { + String body = generateRepositoriesResponse(totalRepos, start, pageSize, isLastPage); + server.enqueue(new MockResponse() + .setHeader("Content-Type", "application/json;charset=UTF-8") + .setBody(body)); + + RepositoryList gsonBBSRepoList = underTest.getRepos(server.url("/").toString(), "token", null, null, start, pageSize); + + int expectedSize = Math.min(pageSize, totalRepos - start + 1); + assertThat(gsonBBSRepoList.getValues()).hasSize(expectedSize); + + // Verify that the correct items are returned + IntStream.rangeClosed(start, start + expectedSize - 1).forEach(i -> { + assertThat(gsonBBSRepoList.getValues()) + .extracting(Repository::getId, Repository::getName, Repository::getSlug) + .contains(tuple((long) i, "Repository_" + i, "repo_" + i)); + }); + + assertThat(gsonBBSRepoList.isLastPage()).isEqualTo(isLastPage); + assertThat(gsonBBSRepoList.getNextPageStart()).isEqualTo(isLastPage ? start : start + expectedSize); + } + + private String generateRepositoriesResponse(int totalRepos, int start, int pageSize, boolean isLastPage) { + int end = Math.min(totalRepos, start + pageSize - 1); + + StringBuilder values = new StringBuilder(); + for (int i = start; i <= end; i++) { + values.append(""" + { + "slug": "repo_%d", + "id": %d, + "name": "Repository_%d", + "project": { + "key": "KEY_%d", + "id": %d, + "name": "Project_%d" + } + } + """.formatted(i, i, i, i, i, i)); + if (i < end) { + values.append(","); + } + } + + return """ + { + "isLastPage": %s, + "nextPageStart": %s, + "values": [%s] + } + """.formatted(isLastPage, isLastPage ? start : end + 1, values.toString()); + } + @Test @UseDataProvider("validStartAndPageSizeProvider") public void get_projects_with_pagination(int start, int pageSize, boolean isLastPage, int totalProjects) { diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/bitbucketserver/ListBitbucketServerProjectsActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/bitbucketserver/ListBitbucketServerProjectsActionIT.java index 8c704a69d49..ac1685f6494 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/bitbucketserver/ListBitbucketServerProjectsActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/bitbucketserver/ListBitbucketServerProjectsActionIT.java @@ -121,6 +121,32 @@ public class ListBitbucketServerProjectsActionIT { validateResponse(response, DEFAULT_START, 10, false, 10 + DEFAULT_START); } + @Test + public void handle_should_list_projects_when_zero_page_size() { + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); + + ListBitbucketserverProjectsWsResponse response = ws.newRequest() + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) + .setParam(PARAM_PAGE_SIZE, "0") + .executeProtobuf(ListBitbucketserverProjectsWsResponse.class); + + validateResponse(response, DEFAULT_START, DEFAULT_PAGE_SIZE, false, DEFAULT_PAGE_SIZE + DEFAULT_START); + } + + @Test + public void handle_should_list_projects_when_negative_page_size() { + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); + + ListBitbucketserverProjectsWsResponse response = ws.newRequest() + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) + .setParam(PARAM_PAGE_SIZE, "-1") + .executeProtobuf(ListBitbucketserverProjectsWsResponse.class); + + validateResponse(response, DEFAULT_START, DEFAULT_PAGE_SIZE, false, DEFAULT_PAGE_SIZE + DEFAULT_START); + } + @Test public void handle_should_return_empty_list_when_out_of_bounds_start() { totalMockedProjects = 30; diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/bitbucketserver/SearchBitbucketServerReposActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/bitbucketserver/SearchBitbucketServerReposActionIT.java index 30268f3a4de..3b859f9a3a7 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/bitbucketserver/SearchBitbucketServerReposActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/bitbucketserver/SearchBitbucketServerReposActionIT.java @@ -19,8 +19,9 @@ */ package org.sonar.server.almintegration.ws.bitbucketserver; -import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -30,7 +31,6 @@ import org.sonar.alm.client.bitbucketserver.Repository; import org.sonar.alm.client.bitbucketserver.RepositoryList; import org.sonar.api.server.ws.WebService; import org.sonar.db.DbTester; -import org.sonar.db.alm.pat.AlmPatDto; import org.sonar.db.alm.setting.AlmSettingDto; import org.sonar.db.alm.setting.ProjectAlmSettingDao; import org.sonar.db.project.ProjectDao; @@ -40,6 +40,7 @@ import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.exceptions.UnauthorizedException; import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; import org.sonar.server.ws.WsActionTester; import org.sonarqube.ws.AlmIntegrations; import org.sonarqube.ws.AlmIntegrations.SearchBitbucketserverReposWsResponse; @@ -48,6 +49,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.tuple; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.sonar.db.alm.integration.pat.AlmPatsTesting.newAlmPatDto; @@ -66,176 +69,256 @@ public class SearchBitbucketServerReposActionIT { private final WsActionTester ws = new WsActionTester( new SearchBitbucketServerReposAction(db.getDbClient(), userSession, bitbucketServerRestClient, projectAlmSettingDao, projectDao)); + private static final String PARAM_ALM_SETTING = "almSetting"; + private static final String PARAM_PROJECT_NAME = "projectName"; + private static final String PARAM_REPOSITORY_NAME = "repositoryName"; + private static final String PARAM_START = "start"; + private static final int DEFAULT_START = 1; // First item has id = 1 + private static final String PARAM_PAGE_SIZE = "pageSize"; + private static final int DEFAULT_PAGE_SIZE = 25; + private static final int MAX_PAGE_SIZE = 100; + + private int totalMockedRepositories = 100; + @Before public void before() { - RepositoryList gsonBBSRepoList = new RepositoryList(); - gsonBBSRepoList.setLastPage(true); - List values = new ArrayList<>(); - values.add(getGsonBBSRepo1()); - values.add(getGsonBBSRepo2()); - gsonBBSRepoList.setValues(values); - when(bitbucketServerRestClient.getRepos(any(), any(), any(), any())).thenReturn(gsonBBSRepoList); + when(bitbucketServerRestClient.getRepos(anyString(), anyString(), any(), any(), any(), anyInt())) + .thenAnswer(invocation -> { + Integer start = invocation.getArgument(4); // Nullable + int pageSize = invocation.getArgument(5); + + start = (start == null) ? DEFAULT_START : start; + return createTestRepositoryList(totalMockedRepositories, start, pageSize); + }); } @Test - public void list_repos() { - UserDto user = db.users().insertUser(); - userSession.logIn(user).addPermission(PROVISION_PROJECTS); - AlmSettingDto almSetting = db.almSettings().insertBitbucketAlmSetting(); - db.almPats().insert(dto -> { - dto.setAlmSettingUuid(almSetting.getUuid()); - dto.setUserUuid(user.getUuid()); - }); - ProjectDto projectDto = db.components().insertPrivateProject(dto -> dto.setKey("proj_key_1").setName("proj_name_1")).getProjectDto(); - db.almSettings().insertBitbucketProjectAlmSetting(almSetting, projectDto, s -> s.setAlmRepo("projectKey2"), s -> s.setAlmSlug("repo-slug-2")); + public void handle_should_list_repositories_when_default_pagination_parameters() { + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); + ProjectDto projectDto = db.components().insertPrivateProject().getProjectDto(); + bindProjectToRepository(almSetting, projectDto, 3); AlmIntegrations.SearchBitbucketserverReposWsResponse response = ws.newRequest() - .setParam("almSetting", almSetting.getKey()) + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) .executeProtobuf(SearchBitbucketserverReposWsResponse.class); - assertThat(response.getIsLastPage()).isTrue(); - assertThat(response.getRepositoriesList()) - .extracting(AlmIntegrations.BBSRepo::getId, AlmIntegrations.BBSRepo::getName, AlmIntegrations.BBSRepo::getSlug, AlmIntegrations.BBSRepo::hasSqProjectKey, - AlmIntegrations.BBSRepo::getSqProjectKey, AlmIntegrations.BBSRepo::getProjectKey) - .containsExactlyInAnyOrder( - tuple(1L, "repoName1", "repo-slug-1", false, "", "projectKey1"), - tuple(3L, "repoName2", "repo-slug-2", true, projectDto.getKey(), "projectKey2")); + validateResponse(response, DEFAULT_START, DEFAULT_PAGE_SIZE, false, DEFAULT_PAGE_SIZE + DEFAULT_START, Map.of(3L, projectDto.getKey())); } @Test - public void return_empty_list_when_no_bbs_repo() { - RepositoryList gsonBBSRepoList = new RepositoryList(); - gsonBBSRepoList.setLastPage(true); - gsonBBSRepoList.setValues(new ArrayList<>()); - when(bitbucketServerRestClient.getRepos(any(), any(), any(), any())).thenReturn(gsonBBSRepoList); - UserDto user = db.users().insertUser(); - userSession.logIn(user).addPermission(PROVISION_PROJECTS); - AlmSettingDto almSetting = db.almSettings().insertBitbucketAlmSetting(); - db.almPats().insert(dto -> { - dto.setAlmSettingUuid(almSetting.getUuid()); - dto.setUserUuid(user.getUuid()); - }); - ProjectDto projectDto = db.components().insertPrivateProject().getProjectDto(); - db.almSettings().insertBitbucketProjectAlmSetting(almSetting, projectDto, s -> s.setAlmRepo("projectKey2"), s -> s.setAlmSlug("repo-slug-2")); + public void handle_should_return_empty_list_when_no_repositories() { + totalMockedRepositories = 0; + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); AlmIntegrations.SearchBitbucketserverReposWsResponse response = ws.newRequest() - .setParam("almSetting", almSetting.getKey()) + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) .executeProtobuf(SearchBitbucketserverReposWsResponse.class); - assertThat(response.getIsLastPage()).isTrue(); - assertThat(response.getRepositoriesList()).isEmpty(); + validateResponse(response, DEFAULT_START, 0, true, DEFAULT_START, Map.of()); } @Test - public void already_imported_detection_does_not_get_confused_by_same_slug_in_different_projects() { - UserDto user = db.users().insertUser(); - userSession.logIn(user).addPermission(PROVISION_PROJECTS); - AlmSettingDto almSetting = db.almSettings().insertBitbucketAlmSetting(); - db.almPats().insert(dto -> { - dto.setAlmSettingUuid(almSetting.getUuid()); - dto.setUserUuid(user.getUuid()); - }); - ProjectDto projectDto = db.components().insertPrivateProject(dto -> dto.setName("proj_1").setKey("proj_key_1")).getProjectDto(); - db.almSettings().insertBitbucketProjectAlmSetting(almSetting, projectDto, s -> s.setAlmRepo("projectKey2"), s -> s.setAlmSlug("repo-slug-2")); - db.almSettings().insertBitbucketProjectAlmSetting(almSetting, db.components().insertPrivateProject(dto -> dto.setName("proj_2").setKey("proj_key_2")).getProjectDto(), s -> s.setAlmRepo("projectKey2"), s -> s.setAlmSlug("repo-slug-2")); + public void handle_should_succeed_when_same_slug_in_different_projects_and_already_imported_detection() { + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); + ProjectDto project1 = db.components().insertPrivateProject(p -> p.setName("pn1").setKey("pk1")).getProjectDto(); + bindProjectToRepository(almSetting, project1, 2); + ProjectDto project2 = db.components().insertPrivateProject(p -> p.setName("pn2").setKey("pk2")).getProjectDto(); + bindProjectToRepository(almSetting, project2, 2); AlmIntegrations.SearchBitbucketserverReposWsResponse response = ws.newRequest() - .setParam("almSetting", almSetting.getKey()) + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) .executeProtobuf(SearchBitbucketserverReposWsResponse.class); - assertThat(response.getIsLastPage()).isTrue(); - assertThat(response.getRepositoriesList()) - .extracting(AlmIntegrations.BBSRepo::getId, AlmIntegrations.BBSRepo::getName, AlmIntegrations.BBSRepo::getSlug, AlmIntegrations.BBSRepo::getSqProjectKey, - AlmIntegrations.BBSRepo::getProjectKey) - .containsExactlyInAnyOrder( - tuple(1L, "repoName1", "repo-slug-1", "", "projectKey1"), - tuple(3L, "repoName2", "repo-slug-2", projectDto.getKey(), "projectKey2")); + validateResponse(response, DEFAULT_START, DEFAULT_PAGE_SIZE, false, DEFAULT_PAGE_SIZE + DEFAULT_START, Map.of(2L, project1.getKey())); } @Test - public void use_projectKey_to_disambiguate_when_multiple_projects_are_binded_on_one_bitbucketserver_repo() { - UserDto user = db.users().insertUser(); - userSession.logIn(user).addPermission(PROVISION_PROJECTS); - AlmSettingDto almSetting = db.almSettings().insertBitbucketAlmSetting(); - db.almPats().insert(dto -> { - dto.setAlmSettingUuid(almSetting.getUuid()); - dto.setUserUuid(user.getUuid()); - }); + public void handle_should_use_project_key_to_disambiguate_when_multiple_projects_are_bound_on_one_bitbucketserver_repository() { + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); ProjectDto project1 = db.components().insertPrivateProject(p -> p.setKey("B")).getProjectDto(); + bindProjectToRepository(almSetting, project1, 4); ProjectDto project2 = db.components().insertPrivateProject(p -> p.setKey("A")).getProjectDto(); - db.almSettings().insertBitbucketProjectAlmSetting(almSetting, project1, s -> s.setAlmRepo("projectKey2"), s -> s.setAlmSlug("repo-slug-2")); - db.almSettings().insertBitbucketProjectAlmSetting(almSetting, project2, s -> s.setAlmRepo("projectKey2"), s -> s.setAlmSlug("repo-slug-2")); + bindProjectToRepository(almSetting, project2, 4); AlmIntegrations.SearchBitbucketserverReposWsResponse response = ws.newRequest() - .setParam("almSetting", almSetting.getKey()) + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) .executeProtobuf(SearchBitbucketserverReposWsResponse.class); - assertThat(response.getIsLastPage()).isTrue(); - assertThat(response.getRepositoriesList()) - .extracting(AlmIntegrations.BBSRepo::getId, AlmIntegrations.BBSRepo::getName, AlmIntegrations.BBSRepo::getSlug, AlmIntegrations.BBSRepo::getSqProjectKey, - AlmIntegrations.BBSRepo::getProjectKey) - .containsExactlyInAnyOrder( - tuple(1L, "repoName1", "repo-slug-1", "", "projectKey1"), - tuple(3L, "repoName2", "repo-slug-2", "A", "projectKey2")); + validateResponse(response, DEFAULT_START, DEFAULT_PAGE_SIZE, false, DEFAULT_PAGE_SIZE + DEFAULT_START, Map.of(4L, "A")); } @Test - public void check_pat_is_missing() { - UserDto user = db.users().insertUser(); - userSession.logIn(user).addPermission(PROVISION_PROJECTS); - AlmSettingDto almSetting = db.almSettings().insertGitHubAlmSetting(); + public void handle_should_list_projects_when_default_pagination_parameters() { + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); + + AlmIntegrations.SearchBitbucketserverReposWsResponse response = ws.newRequest() + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) + .executeProtobuf(AlmIntegrations.SearchBitbucketserverReposWsResponse.class); + + validateResponse(response, DEFAULT_START, DEFAULT_PAGE_SIZE, false, DEFAULT_PAGE_SIZE + DEFAULT_START, Map.of()); + } + + @Test + public void handle_should_list_projects_when_custom_start() { + totalMockedRepositories = 55; + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); + + AlmIntegrations.SearchBitbucketserverReposWsResponse response = ws.newRequest() + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) + .setParam(PARAM_START, "51") + .executeProtobuf(AlmIntegrations.SearchBitbucketserverReposWsResponse.class); + + validateResponse(response, 51, 5, true, 51, Map.of()); + } + + @Test + public void handle_should_list_projects_when_custom_page_size() { + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); + + AlmIntegrations.SearchBitbucketserverReposWsResponse response = ws.newRequest() + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) + .setParam(PARAM_PAGE_SIZE, "10") + .executeProtobuf(AlmIntegrations.SearchBitbucketserverReposWsResponse.class); + + validateResponse(response, DEFAULT_START, 10, false, 10 + DEFAULT_START, Map.of()); + } + + @Test + public void handle_should_list_projects_when_zero_page_size() { + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); + + AlmIntegrations.SearchBitbucketserverReposWsResponse response = ws.newRequest() + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) + .setParam(PARAM_PAGE_SIZE, "0") + .executeProtobuf(AlmIntegrations.SearchBitbucketserverReposWsResponse.class); + + validateResponse(response, DEFAULT_START, DEFAULT_PAGE_SIZE, false, DEFAULT_PAGE_SIZE + DEFAULT_START, Map.of()); + } + + @Test + public void handle_should_list_projects_when_negative_page_size() { + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); + + AlmIntegrations.SearchBitbucketserverReposWsResponse response = ws.newRequest() + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) + .setParam(PARAM_PAGE_SIZE, "-1") + .executeProtobuf(AlmIntegrations.SearchBitbucketserverReposWsResponse.class); + + validateResponse(response, DEFAULT_START, DEFAULT_PAGE_SIZE, false, DEFAULT_PAGE_SIZE + DEFAULT_START, Map.of()); + } + + @Test + public void handle_should_return_empty_list_when_out_of_bounds_start() { + totalMockedRepositories = 30; + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); + + AlmIntegrations.SearchBitbucketserverReposWsResponse response = ws.newRequest() + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) + .setParam(PARAM_START, "50") // Out-of-bounds start + .setParam(PARAM_PAGE_SIZE, "10") + .executeProtobuf(AlmIntegrations.SearchBitbucketserverReposWsResponse.class); + + validateResponse(response, 50, 0, true, 50, Map.of()); + } + + @Test + public void handle_should_throw_illegal_argument_exception_when_invalid_start() { + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); + + TestRequest request = ws.newRequest() + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) + .setParam(PARAM_START, "notAnInt"); + + assertThatThrownBy(request::execute) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("The '%s' parameter cannot be parsed as an integer value: %s".formatted(PARAM_START, "notAnInt")); + } + + @Test + public void handle_should_throw_illegal_argument_exception_when_out_of_bounds_page_size() { + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); + + TestRequest request = ws.newRequest() + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) + .setParam(PARAM_PAGE_SIZE, String.valueOf(MAX_PAGE_SIZE + 1)); + + assertThatThrownBy(request::execute) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("'%s' value (%s) must be less than %s".formatted(PARAM_PAGE_SIZE, MAX_PAGE_SIZE + 1, MAX_PAGE_SIZE)); + } + + @Test + public void handle_should_throw_illegal_argument_exception_when_invalid_page_size() { + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); + + TestRequest request = ws.newRequest() + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) + .setParam(PARAM_PAGE_SIZE, "notAnInt"); + + assertThatThrownBy(request::execute) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("'%s' value '%s' cannot be parsed as an integer".formatted(PARAM_PAGE_SIZE, "notAnInt")); + } + + @Test + public void handle_should_throw_illegal_argument_exception_when_pat_is_missing() { + createUserWithPermissions(); + AlmSettingDto almSetting = db.almSettings().insertBitbucketAlmSetting(); + + TestRequest request = ws.newRequest().setParam(PARAM_ALM_SETTING, almSetting.getKey()); - assertThatThrownBy(() -> { - ws.newRequest() - .setParam("almSetting", almSetting.getKey()) - .execute(); - }) + assertThatThrownBy(request::execute) .isInstanceOf(IllegalArgumentException.class) .hasMessage("No personal access token found"); } @Test - public void fail_check_pat_alm_setting_not_found() { - UserDto user = db.users().insertUser(); - userSession.logIn(user).addPermission(PROVISION_PROJECTS); - AlmPatDto almPatDto = newAlmPatDto(); - db.getDbClient().almPatDao().insert(db.getSession(), almPatDto, user.getLogin(), null); - - assertThatThrownBy(() -> { - ws.newRequest() - .setParam("almSetting", "testKey") - .execute(); - }) + public void handle_should_throw_not_found_exception_when_alm_setting_is_missing() { + UserDto user = createUserWithPermissions(); + db.getDbClient().almPatDao().insert(db.getSession(), newAlmPatDto(), user.getLogin(), null); + + TestRequest request = ws.newRequest().setParam(PARAM_ALM_SETTING, "testKey"); + + assertThatThrownBy(request::execute) .isInstanceOf(NotFoundException.class) - .hasMessage("DevOps Platform Setting 'testKey' not found"); + .hasMessage("DevOps Platform Setting '%s' not found".formatted("testKey")); } @Test - public void fail_when_not_logged_in() { - assertThatThrownBy(() -> { - ws.newRequest() - .setParam("almSetting", "anyvalue") - .execute(); - }) + public void handle_should_throw_unauthorized_exception_when_user_is_not_logged_in() { + TestRequest request = ws.newRequest().setParam(PARAM_ALM_SETTING, "any"); + + assertThatThrownBy(request::execute) .isInstanceOf(UnauthorizedException.class); } @Test - public void fail_when_no_creation_project_permission() { + public void handle_should_throw_forbidden_exception_when_user_has_no_permission() { UserDto user = db.users().insertUser(); userSession.logIn(user); - - assertThatThrownBy(() -> { - ws.newRequest() - .setParam("almSetting", "anyvalue") - .execute(); - }) + + TestRequest request = ws.newRequest().setParam(PARAM_ALM_SETTING, "any"); + + assertThatThrownBy(request::execute) .isInstanceOf(ForbiddenException.class) .hasMessage("Insufficient privileges"); } @Test - public void definition() { + public void definition_should_be_documented() { WebService.Action def = ws.getDef(); assertThat(def.since()).isEqualTo("8.2"); @@ -244,35 +327,88 @@ public class SearchBitbucketServerReposActionIT { assertThat(def.params()) .extracting(WebService.Param::key, WebService.Param::isRequired) .containsExactlyInAnyOrder( - tuple("almSetting", true), - tuple("projectName", false), - tuple("repositoryName", false)); + tuple(PARAM_ALM_SETTING, true), + tuple(PARAM_PROJECT_NAME, false), + tuple(PARAM_REPOSITORY_NAME, false), + tuple(PARAM_START, false), + tuple(PARAM_PAGE_SIZE, false)); } - private Repository getGsonBBSRepo1() { - Repository gsonBBSRepo1 = new Repository(); - gsonBBSRepo1.setId(1L); - gsonBBSRepo1.setName("repoName1"); - Project project1 = new Project(); - project1.setName("projectName1"); - project1.setKey("projectKey1"); - project1.setId(2L); - gsonBBSRepo1.setProject(project1); - gsonBBSRepo1.setSlug("repo-slug-1"); - return gsonBBSRepo1; + // region utility methods + + private UserDto createUserWithPermissions() { + UserDto user = db.users().insertUser(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + return user; } - private Repository getGsonBBSRepo2() { - Repository gsonBBSRepo = new Repository(); - gsonBBSRepo.setId(3L); - gsonBBSRepo.setName("repoName2"); - Project project = new Project(); - project.setName("projectName2"); - project.setKey("projectKey2"); - project.setId(4L); - gsonBBSRepo.setProject(project); - gsonBBSRepo.setSlug("repo-slug-2"); - return gsonBBSRepo; + private AlmSettingDto createAlmSettingWithPat(UserDto user) { + AlmSettingDto almSetting = db.almSettings().insertBitbucketAlmSetting(); + db.almPats().insert(dto -> { + dto.setAlmSettingUuid(almSetting.getUuid()); + dto.setUserUuid(user.getUuid()); + }); + return almSetting; } + private RepositoryList createTestRepositoryList(int nbRepositories, int start, int pageSize) { + int end = Math.min(start + pageSize - 1, nbRepositories); + + List repositories = IntStream.rangeClosed(start, end) + .mapToObj(this::createTestRepository) + .toList(); + + boolean isLastPage = end == nbRepositories; + int nextPageStart = isLastPage ? start : end + 1; + + return new RepositoryList(isLastPage, nextPageStart, repositories.size(), repositories); + } + + private Repository createTestRepository(int id) { + return new Repository("repository-slug-" + id, "repository-name-" + id, id, createTestProject(id)); + } + + private Project createTestProject(int id) { + return new Project("project-key-" + id, "project-name-" + id, id); + } + + private void bindProjectToRepository(AlmSettingDto almSetting, ProjectDto project, int id) { + db.almSettings().insertBitbucketProjectAlmSetting( + almSetting, + project, + s -> s.setAlmRepo("project-key-" + id), + s -> s.setAlmSlug("repository-slug-" + id)); + } + + private void validateResponse(SearchBitbucketserverReposWsResponse response, int start, int pageSize, boolean lastPage, int nextPageStart, + Map boundProjectKeyByIds) { + int expectedProjectCount = Math.max(0, Math.min(pageSize, totalMockedRepositories - start + 1)); + + assertThat(response.getPaging().getPageSize()).isEqualTo(expectedProjectCount); + assertThat(response.getIsLastPage()).isEqualTo(lastPage); + assertThat(response.getNextPageStart()).isEqualTo(nextPageStart); + assertThat(response.getRepositoriesList()).hasSize(expectedProjectCount); + + assertThat(response.getRepositoriesList()) + .extracting( + AlmIntegrations.BBSRepo::getId, + AlmIntegrations.BBSRepo::getName, + AlmIntegrations.BBSRepo::getSlug, + AlmIntegrations.BBSRepo::hasSqProjectKey, + AlmIntegrations.BBSRepo::getSqProjectKey, + AlmIntegrations.BBSRepo::getProjectKey) + .containsExactlyElementsOf( + IntStream.rangeClosed(start, start + expectedProjectCount - 1) + .mapToObj(id -> tuple( + (long) id, + "repository-name-" + id, + "repository-slug-" + id, + boundProjectKeyByIds.containsKey((long) id), + boundProjectKeyByIds.getOrDefault((long) id, ""), + "project-key-" + id)) + .toList()); + } + + // endregion utility methods + } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketserver/ListBitbucketServerProjectsAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketserver/ListBitbucketServerProjectsAction.java index 0755702f3c0..48fa73d6129 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketserver/ListBitbucketServerProjectsAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketserver/ListBitbucketServerProjectsAction.java @@ -77,7 +77,7 @@ public class ListBitbucketServerProjectsAction implements AlmIntegrationsWsActio action.createParam(PARAM_START) .setExampleValue(2154) - .setDescription("Start number for the page (inclusive). If not passed, first page is assumed."); + .setDescription("Start number for the page (inclusive). If not passed, the first page is assumed."); action.createParam(PARAM_PAGE_SIZE) .setDefaultValue(DEFAULT_PAGE_SIZE) @@ -96,7 +96,10 @@ public class ListBitbucketServerProjectsAction implements AlmIntegrationsWsActio String almSettingKey = request.mandatoryParam(PARAM_ALM_SETTING); Integer start = request.paramAsInt(PARAM_START); - int pageSize = Optional.ofNullable(request.paramAsInt(PARAM_PAGE_SIZE)).orElse(DEFAULT_PAGE_SIZE); + int pageSize = Optional.ofNullable(request.paramAsInt(PARAM_PAGE_SIZE)) + // non-positive should fallback to default + .filter(ps -> ps > 0) + .orElse(DEFAULT_PAGE_SIZE); String userUuid = requireNonNull(userSession.getUuid(), "User UUID is not null"); try (DbSession dbSession = dbClient.openSession(false)) { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketserver/SearchBitbucketServerReposAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketserver/SearchBitbucketServerReposAction.java index 1ac2d0d3dfa..51731bd01f1 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketserver/SearchBitbucketServerReposAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketserver/SearchBitbucketServerReposAction.java @@ -46,6 +46,7 @@ import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.user.UserSession; import org.sonarqube.ws.AlmIntegrations.BBSRepo; import org.sonarqube.ws.AlmIntegrations.SearchBitbucketserverReposWsResponse; +import org.sonarqube.ws.Common; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toMap; @@ -57,6 +58,10 @@ public class SearchBitbucketServerReposAction implements AlmIntegrationsWsAction private static final String PARAM_ALM_SETTING = "almSetting"; private static final String PARAM_REPO_NAME = "repositoryName"; private static final String PARAM_PROJECT_NAME = "projectName"; + private static final String PARAM_START = "start"; + private static final String PARAM_PAGE_SIZE = "pageSize"; + private static final int DEFAULT_PAGE_SIZE = 25; + private static final int MAX_PAGE_SIZE = 100; private final DbClient dbClient; private final UserSession userSession; @@ -95,6 +100,15 @@ public class SearchBitbucketServerReposAction implements AlmIntegrationsWsAction .setRequired(false) .setMaximumLength(200) .setDescription("Repository name filter"); + + action.createParam(PARAM_START) + .setExampleValue(2154) + .setDescription("Start number for the page (inclusive). If not passed, the first page is assumed."); + + action.createParam(PARAM_PAGE_SIZE) + .setDefaultValue(DEFAULT_PAGE_SIZE) + .setMaximumValue(MAX_PAGE_SIZE) + .setDescription("Number of items to return."); } @Override @@ -104,30 +118,40 @@ public class SearchBitbucketServerReposAction implements AlmIntegrationsWsAction } private SearchBitbucketserverReposWsResponse doHandle(Request request) { + userSession.checkLoggedIn().checkPermission(PROVISION_PROJECTS); + + String almSettingKey = request.mandatoryParam(PARAM_ALM_SETTING); + Integer start = request.paramAsInt(PARAM_START); + int pageSize = Optional.ofNullable(request.paramAsInt(PARAM_PAGE_SIZE)) + // non-positive should fallback to default + .filter(ps -> ps > 0) + .orElse(DEFAULT_PAGE_SIZE); + String userUuid = requireNonNull(userSession.getUuid(), "User UUID cannot be null"); + String projectKey = request.param(PARAM_PROJECT_NAME); + String repoName = request.param(PARAM_REPO_NAME); try (DbSession dbSession = dbClient.openSession(false)) { - userSession.checkLoggedIn().checkPermission(PROVISION_PROJECTS); - - String almSettingKey = request.mandatoryParam(PARAM_ALM_SETTING); - String userUuid = requireNonNull(userSession.getUuid(), "User UUID cannot be null"); AlmSettingDto almSettingDto = dbClient.almSettingDao().selectByKey(dbSession, almSettingKey) .orElseThrow(() -> new NotFoundException(String.format("DevOps Platform Setting '%s' not found", almSettingKey))); Optional almPatDto = dbClient.almPatDao().selectByUserAndAlmSetting(dbSession, userUuid, almSettingDto); - String projectKey = request.param(PARAM_PROJECT_NAME); - String repoName = request.param(PARAM_REPO_NAME); String pat = almPatDto.map(AlmPatDto::getPersonalAccessToken).orElseThrow(() -> new IllegalArgumentException("No personal access token found")); String url = requireNonNull(almSettingDto.getUrl(), "DevOps Platform url cannot be null"); - RepositoryList gsonBBSRepoList = bitbucketServerRestClient.getRepos(url, pat, projectKey, repoName); + RepositoryList gsonBBSRepoList = bitbucketServerRestClient.getRepos(url, pat, projectKey, repoName, start, pageSize); Map sqProjectsKeyByBBSKey = getSqProjectsKeyByBBSKey(dbSession, almSettingDto, gsonBBSRepoList); - List bbsRepos = gsonBBSRepoList.getValues().stream().map(gsonBBSRepo -> toBBSRepo(gsonBBSRepo, sqProjectsKeyByBBSKey)) + List bbsRepos = gsonBBSRepoList.getValues().stream() + .map(gsonBBSRepo -> toBBSRepo(gsonBBSRepo, sqProjectsKeyByBBSKey)) .toList(); - SearchBitbucketserverReposWsResponse.Builder builder = SearchBitbucketserverReposWsResponse.newBuilder() + return SearchBitbucketserverReposWsResponse.newBuilder() .setIsLastPage(gsonBBSRepoList.isLastPage()) - .addAllRepositories(bbsRepos); - return builder.build(); + .setNextPageStart(gsonBBSRepoList.getNextPageStart()) + .setPaging(Common.Paging.newBuilder() + .setPageSize(gsonBBSRepoList.getSize()) + .build()) + .addAllRepositories(bbsRepos) + .build(); } } @@ -151,7 +175,8 @@ public class SearchBitbucketServerReposAction implements AlmIntegrationsWsAction .setSlug(gsonBBSRepo.getSlug()) .setId(gsonBBSRepo.getId()) .setName(gsonBBSRepo.getName()) - .setProjectKey(gsonBBSRepo.getProject().getKey()); + .setProjectKey(gsonBBSRepo.getProject().getKey()) + .setProjectName(gsonBBSRepo.getProject().getName()); String sqProjectKey = sqProjectsKeyByBBSKey.get(customKey(gsonBBSRepo)); if (sqProjectKey != null) { -- cgit v1.2.3