aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorJulien Camus <julien.camus@sonarsource.com>2024-12-10 16:00:22 +0100
committerSteve Marion <steve.marion@sonarsource.com>2024-12-18 11:13:21 +0100
commit48d5ec5040eccf6e7638170135426f247807a585 (patch)
treebee1e1787e6f33e43e3f7bdbdd584bf000b47b61 /server
parentd639a965bce7acafb004906cd07a8f0b5f7af993 (diff)
downloadsonarqube-48d5ec5040eccf6e7638170135426f247807a585.tar.gz
sonarqube-48d5ec5040eccf6e7638170135426f247807a585.zip
SONAR-23845 Add pagination to SearchBitbucketServerReposAction
Diffstat (limited to 'server')
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClient.java5
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/RepositoryList.java43
-rw-r--r--server/sonar-alm-client/src/test/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClientTest.java57
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/bitbucketserver/ListBitbucketServerProjectsActionIT.java26
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/bitbucketserver/SearchBitbucketServerReposActionIT.java418
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketserver/ListBitbucketServerProjectsAction.java7
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketserver/SearchBitbucketServerReposAction.java49
7 files changed, 427 insertions, 178 deletions
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<Repository> 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<Repository> values) {
- this.isLastPage = isLastPage;
+ public RepositoryList(boolean lastPage, int nextPageStart, int size, List<Repository> 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<Repository> getValues() {
return values;
}
- public RepositoryList setValues(List<Repository> values) {
+ public void setValues(List<Repository> 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,
@@ -702,6 +702,61 @@ public class BitbucketServerRestClientTest {
@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) {
String body = generateProjectsResponse(totalProjects, start, pageSize, isLastPage);
server.enqueue(new MockResponse()
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
@@ -122,6 +122,32 @@ public class ListBitbucketServerProjectsActionIT {
}
@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;
UserDto user = createUserWithPermissions();
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<Repository> 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<Repository> 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<Long, String> 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> 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<String, String> sqProjectsKeyByBBSKey = getSqProjectsKeyByBBSKey(dbSession, almSettingDto, gsonBBSRepoList);
- List<BBSRepo> bbsRepos = gsonBBSRepoList.getValues().stream().map(gsonBBSRepo -> toBBSRepo(gsonBBSRepo, sqProjectsKeyByBBSKey))
+ List<BBSRepo> 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) {