diff options
Diffstat (limited to 'server')
8 files changed, 611 insertions, 359 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 9b7bbbdb59e..83ec942df8b 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 @@ -101,8 +101,9 @@ public class BitbucketServerRestClient { return doGet(token, url, body -> buildGson().fromJson(body, RepositoryList.class)); } - public ProjectList getProjects(String serverUrl, String token) { - HttpUrl url = buildUrl(serverUrl, "/rest/api/1.0/projects"); + public ProjectList getProjects(String serverUrl, String token, @Nullable Integer start, int pageSize) { + String startOrEmpty = Optional.ofNullable(start).map(String::valueOf).orElse(""); + HttpUrl url = buildUrl(serverUrl, format("/rest/api/1.0/projects?start=%s&limit=%s", startOrEmpty, pageSize)); return doGet(token, url, body -> buildGson().fromJson(body, ProjectList.class)); } @@ -149,7 +150,7 @@ public class BitbucketServerRestClient { handleHttpErrorIfAny(response.isSuccessful(), response.code(), bodyString); return bodyString; } catch (IOException e) { - LOG.info(UNABLE_TO_CONTACT_BITBUCKET_SERVER + ": " + e.getMessage(), e); + LOG.info(UNABLE_TO_CONTACT_BITBUCKET_SERVER + ": {}", e.getMessage(), e); throw new IllegalArgumentException(UNABLE_TO_CONTACT_BITBUCKET_SERVER, e); } } @@ -189,9 +190,16 @@ public class BitbucketServerRestClient { } protected static boolean equals(@Nullable MediaType first, @Nullable MediaType second) { - String s1 = first == null ? null : first.toString().toLowerCase(ENGLISH).replace(" ", ""); - String s2 = second == null ? null : second.toString().toLowerCase(ENGLISH).replace(" ", ""); - return s1 != null && s2 != null && s1.equals(s2); + String s1 = convertMediaTypeToString(first); + String s2 = convertMediaTypeToString(second); + return s1 != null && s1.equals(s2); + } + + private static String convertMediaTypeToString(@Nullable MediaType mediaType) { + return Optional.ofNullable(mediaType) + .map(MediaType::toString) + .map(s -> s.toLowerCase(ENGLISH).replace(" ", "")) + .orElse(null); } protected static String getErrorMessage(String bodyString) { diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/ProjectList.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/ProjectList.java index 4d0873e7b19..5b5063459ac 100644 --- a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/ProjectList.java +++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/ProjectList.java @@ -20,36 +20,54 @@ package org.sonar.alm.client.bitbucketserver; import com.google.gson.annotations.SerializedName; -import java.util.ArrayList; import java.util.List; public class ProjectList { + @SerializedName("isLastPage") + private boolean lastPage; + + @SerializedName("nextPageStart") + private int nextPageStart; + + @SerializedName("size") + private int size; + @SerializedName("values") private List<Project> values; - public ProjectList() { + private ProjectList() { // http://stackoverflow.com/a/18645370/229031 - this(new ArrayList<>()); + this(true, 0, 0, List.of()); } - public ProjectList(List<Project> values) { + public ProjectList(boolean lastPage, int nextPageStart, int size, List<Project> values) { + this.lastPage = lastPage; + this.nextPageStart = nextPageStart; + this.size = size; this.values = values; } - public List<Project> getValues() { - return values; + public boolean isLastPage() { + return lastPage; } - public ProjectList setValues(List<Project> values) { - this.values = values; - return this; + public int getNextPageStart() { + return nextPageStart; + } + + public int getSize() { + return size; + } + + public List<Project> getValues() { + return values; } @Override public String toString() { - return "{" + - "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 876899d7ea4..dc719609ab1 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 @@ -19,12 +19,12 @@ */ package org.sonar.alm.client.bitbucketserver; -import java.io.IOException; -import java.util.function.Function; - import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.io.IOException; +import java.util.function.Function; +import java.util.stream.IntStream; import okhttp3.MediaType; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -45,31 +45,33 @@ import static org.assertj.core.api.Assertions.tuple; @RunWith(DataProviderRunner.class) public class BitbucketServerRestClientTest { private final MockWebServer server = new MockWebServer(); - private static final String REPOS_BODY = "{\n" + - " \"isLastPage\": true,\n" + - " \"values\": [\n" + - " {\n" + - " \"slug\": \"banana\",\n" + - " \"id\": 2,\n" + - " \"name\": \"banana\",\n" + - " \"project\": {\n" + - " \"key\": \"HOY\",\n" + - " \"id\": 2,\n" + - " \"name\": \"hoy\"\n" + - " }\n" + - " },\n" + - " {\n" + - " \"slug\": \"potato\",\n" + - " \"id\": 1,\n" + - " \"name\": \"potato\",\n" + - " \"project\": {\n" + - " \"key\": \"HEY\",\n" + - " \"id\": 1,\n" + - " \"name\": \"hey\"\n" + - " }\n" + - " }\n" + - " ]\n" + - "}"; + private static final String REPOS_BODY = """ + { + "isLastPage": true, + "values": [ + { + "slug": "banana", + "id": 2, + "name": "banana", + "project": { + "key": "HOY", + "id": 2, + "name": "hoy" + } + }, + { + "slug": "potato", + "id": 1, + "name": "potato", + "project": { + "key": "HEY", + "id": 1, + "name": "hey" + } + } + ] + } + """; private static final String STATUS_BODY = "{\"state\": \"RUNNING\"}"; @Rule @@ -93,37 +95,39 @@ public class BitbucketServerRestClientTest { public void get_repos() { server.enqueue(new MockResponse() .setHeader("Content-Type", "application/json;charset=UTF-8") - .setBody("{\n" + - " \"isLastPage\": true,\n" + - " \"values\": [\n" + - " {\n" + - " \"slug\": \"banana\",\n" + - " \"id\": 2,\n" + - " \"name\": \"banana\",\n" + - " \"project\": {\n" + - " \"key\": \"HOY\",\n" + - " \"id\": 2,\n" + - " \"name\": \"hoy\"\n" + - " }\n" + - " },\n" + - " {\n" + - " \"slug\": \"potato\",\n" + - " \"id\": 1,\n" + - " \"name\": \"potato\",\n" + - " \"project\": {\n" + - " \"key\": \"HEY\",\n" + - " \"id\": 1,\n" + - " \"name\": \"hey\"\n" + - " }\n" + - " }\n" + - " ]\n" + - "}")); + .setBody(""" + { + "isLastPage": true, + "values": [ + { + "slug": "banana", + "id": 2, + "name": "banana", + "project": { + "key": "HOY", + "id": 2, + "name": "hoy" + } + }, + { + "slug": "potato", + "id": 1, + "name": "potato", + "project": { + "key": "HEY", + "id": 1, + "name": "hey" + } + } + ] + } + """)); RepositoryList gsonBBSRepoList = underTest.getRepos(server.url("/").toString(), "token", "", ""); assertThat(gsonBBSRepoList.isLastPage()).isTrue(); assertThat(gsonBBSRepoList.getValues()).hasSize(2); assertThat(gsonBBSRepoList.getValues()).extracting(Repository::getId, Repository::getName, Repository::getSlug, - g -> g.getProject().getId(), g -> g.getProject().getKey(), g -> g.getProject().getName()) + g -> g.getProject().getId(), g -> g.getProject().getKey(), g -> g.getProject().getName()) .containsExactlyInAnyOrder( tuple(2L, "banana", "banana", 2L, "HOY", "hoy"), tuple(1L, "potato", "potato", 1L, "HEY", "hey")); @@ -133,37 +137,39 @@ public class BitbucketServerRestClientTest { public void get_recent_repos() { server.enqueue(new MockResponse() .setHeader("Content-Type", "application/json;charset=UTF-8") - .setBody("{\n" + - " \"isLastPage\": true,\n" + - " \"values\": [\n" + - " {\n" + - " \"slug\": \"banana\",\n" + - " \"id\": 2,\n" + - " \"name\": \"banana\",\n" + - " \"project\": {\n" + - " \"key\": \"HOY\",\n" + - " \"id\": 2,\n" + - " \"name\": \"hoy\"\n" + - " }\n" + - " },\n" + - " {\n" + - " \"slug\": \"potato\",\n" + - " \"id\": 1,\n" + - " \"name\": \"potato\",\n" + - " \"project\": {\n" + - " \"key\": \"HEY\",\n" + - " \"id\": 1,\n" + - " \"name\": \"hey\"\n" + - " }\n" + - " }\n" + - " ]\n" + - "}")); + .setBody(""" + { + "isLastPage": true, + "values": [ + { + "slug": "banana", + "id": 2, + "name": "banana", + "project": { + "key": "HOY", + "id": 2, + "name": "hoy" + } + }, + { + "slug": "potato", + "id": 1, + "name": "potato", + "project": { + "key": "HEY", + "id": 1, + "name": "hey" + } + } + ] + } + """)); RepositoryList gsonBBSRepoList = underTest.getRecentRepo(server.url("/").toString(), "token"); assertThat(gsonBBSRepoList.isLastPage()).isTrue(); assertThat(gsonBBSRepoList.getValues()).hasSize(2); assertThat(gsonBBSRepoList.getValues()).extracting(Repository::getId, Repository::getName, Repository::getSlug, - g -> g.getProject().getId(), g -> g.getProject().getKey(), g -> g.getProject().getName()) + g -> g.getProject().getId(), g -> g.getProject().getKey(), g -> g.getProject().getName()) .containsExactlyInAnyOrder( tuple(2L, "banana", "banana", 2L, "HOY", "hoy"), tuple(1L, "potato", "potato", 1L, "HEY", "hey")); @@ -173,17 +179,18 @@ public class BitbucketServerRestClientTest { public void get_repo() { server.enqueue(new MockResponse() .setHeader("Content-Type", "application/json;charset=UTF-8") - .setBody( - " {" + - " \"slug\": \"banana-slug\"," + - " \"id\": 2,\n" + - " \"name\": \"banana\"," + - " \"project\": {\n" + - " \"key\": \"HOY\"," + - " \"id\": 3,\n" + - " \"name\": \"hoy\"" + - " }" + - " }")); + .setBody(""" + { + "slug": "banana-slug", + "id": 2, + "name": "banana", + "project": { + "key": "HOY", + "id": 3, + "name": "hoy" + } + } + """)); Repository repository = underTest.getRepo(server.url("/").toString(), "token", "", ""); assertThat(repository.getId()).isEqualTo(2L); @@ -198,23 +205,25 @@ public class BitbucketServerRestClientTest { public void get_projects() { server.enqueue(new MockResponse() .setHeader("Content-Type", "application/json;charset=UTF-8") - .setBody("{\n" + - " \"isLastPage\": true,\n" + - " \"values\": [\n" + - " {\n" + - " \"key\": \"HEY\",\n" + - " \"id\": 1,\n" + - " \"name\": \"hey\"\n" + - " },\n" + - " {\n" + - " \"key\": \"HOY\",\n" + - " \"id\": 2,\n" + - " \"name\": \"hoy\"\n" + - " }\n" + - " ]\n" + - "}")); - - final ProjectList gsonBBSProjectList = underTest.getProjects(server.url("/").toString(), "token"); + .setBody(""" + { + "isLastPage": true, + "values": [ + { + "key": "HEY", + "id": 1, + "name": "hey" + }, + { + "key": "HOY", + "id": 2, + "name": "hoy" + } + ] + } + """)); + + final ProjectList gsonBBSProjectList = underTest.getProjects(server.url("/").toString(), "token", null, 25); assertThat(gsonBBSProjectList.getValues()).hasSize(2); assertThat(gsonBBSProjectList.getValues()).extracting(Project::getId, Project::getKey, Project::getName) .containsExactlyInAnyOrder( @@ -225,26 +234,28 @@ public class BitbucketServerRestClientTest { @Test public void get_projects_failed() { server.enqueue(new MockResponse() - .setBody(new Buffer().write(new byte[4096])) - .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY)); + .setBody(new Buffer().write(new byte[4096])) + .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY)); String serverUrl = server.url("/").toString(); - assertThatThrownBy(() -> underTest.getProjects(serverUrl, "token")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Unable to contact Bitbucket server"); + assertThatThrownBy(() -> underTest.getProjects(serverUrl, "token", null, 25)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unable to contact Bitbucket server"); assertThat(String.join(", ", logTester.logs())).contains("Unable to contact Bitbucket server"); } @Test public void getBranches_given0Branches_returnEmptyList() { - String bodyWith0Branches = "{\n" + - " \"size\": 0,\n" + - " \"limit\": 25,\n" + - " \"isLastPage\": true,\n" + - " \"values\": [],\n" + - " \"start\": 0\n" + - "}"; + String bodyWith0Branches = """ + { + "size": 0, + "limit": 25, + "isLastPage": true, + "values": [], + "start": 0 + } + """; server.enqueue(new MockResponse() .setHeader("Content-Type", "application/json;charset=UTF-8") .setBody(bodyWith0Branches)); @@ -256,20 +267,22 @@ public class BitbucketServerRestClientTest { @Test public void getBranches_given1Branch_returnListWithOneBranch() { - String bodyWith1Branch = "{\n" + - " \"size\": 1,\n" + - " \"limit\": 25,\n" + - " \"isLastPage\": true,\n" + - " \"values\": [{\n" + - " \"id\": \"refs/heads/demo\",\n" + - " \"displayId\": \"demo\",\n" + - " \"type\": \"BRANCH\",\n" + - " \"latestCommit\": \"3e30a6701af6f29f976e9a6609a6076b32a69ac3\",\n" + - " \"latestChangeset\": \"3e30a6701af6f29f976e9a6609a6076b32a69ac3\",\n" + - " \"isDefault\": false\n" + - " }],\n" + - " \"start\": 0\n" + - "}"; + String bodyWith1Branch = """ + { + "size": 1, + "limit": 25, + "isLastPage": true, + "values": [{ + "id": "refs/heads/demo", + "displayId": "demo", + "type": "BRANCH", + "latestCommit": "3e30a6701af6f29f976e9a6609a6076b32a69ac3", + "latestChangeset": "3e30a6701af6f29f976e9a6609a6076b32a69ac3", + "isDefault": false + }], + "start": 0 + } + """; server.enqueue(new MockResponse() .setHeader("Content-Type", "application/json;charset=UTF-8") .setBody(bodyWith1Branch)); @@ -285,27 +298,29 @@ public class BitbucketServerRestClientTest { @Test public void getBranches_given2Branches_returnListWithTwoBranches() { - String bodyWith2Branches = "{\n" + - " \"size\": 2,\n" + - " \"limit\": 25,\n" + - " \"isLastPage\": true,\n" + - " \"values\": [{\n" + - " \"id\": \"refs/heads/demo\",\n" + - " \"displayId\": \"demo\",\n" + - " \"type\": \"BRANCH\",\n" + - " \"latestCommit\": \"3e30a6701af6f29f976e9a6609a6076b32a69ac3\",\n" + - " \"latestChangeset\": \"3e30a6701af6f29f976e9a6609a6076b32a69ac3\",\n" + - " \"isDefault\": false\n" + - " }, {\n" + - " \"id\": \"refs/heads/master\",\n" + - " \"displayId\": \"master\",\n" + - " \"type\": \"BRANCH\",\n" + - " \"latestCommit\": \"66633864d27c531ff43892f6dfea6d91632682fa\",\n" + - " \"latestChangeset\": \"66633864d27c531ff43892f6dfea6d91632682fa\",\n" + - " \"isDefault\": true\n" + - " }],\n" + - " \"start\": 0\n" + - "}"; + String bodyWith2Branches = """ + { + "size": 2, + "limit": 25, + "isLastPage": true, + "values": [{ + "id": "refs/heads/demo", + "displayId": "demo", + "type": "BRANCH", + "latestCommit": "3e30a6701af6f29f976e9a6609a6076b32a69ac3", + "latestChangeset": "3e30a6701af6f29f976e9a6609a6076b32a69ac3", + "isDefault": false + }, { + "id": "refs/heads/master", + "displayId": "master", + "type": "BRANCH", + "latestCommit": "66633864d27c531ff43892f6dfea6d91632682fa", + "latestChangeset": "66633864d27c531ff43892f6dfea6d91632682fa", + "isDefault": true + }], + "start": 0 + } + """; server.enqueue(new MockResponse() .setHeader("Content-Type", "application/json;charset=UTF-8") .setBody(bodyWith2Branches)); @@ -318,8 +333,8 @@ public class BitbucketServerRestClientTest { @Test public void invalid_empty_url() { assertThatThrownBy(() -> BitbucketServerRestClient.buildUrl(null, "")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("url must start with http:// or https://"); + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("url must start with http:// or https://"); } @Test @@ -345,7 +360,7 @@ public class BitbucketServerRestClientTest { @Test public void fail_json_error_handling() { - assertThatThrownBy(() -> underTest.applyHandler(body -> underTest.buildGson().fromJson(body, Object.class), "not json")) + assertThatThrownBy(() -> BitbucketServerRestClient.applyHandler(body -> BitbucketServerRestClient.buildGson().fromJson(body, Object.class), "not json")) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unable to contact Bitbucket server, got an unexpected response"); assertThat(String.join(", ", logTester.logs())) @@ -355,9 +370,9 @@ public class BitbucketServerRestClientTest { @Test public void validate_handler_call_on_empty_body() { server.enqueue(new MockResponse().setResponseCode(200) - .setBody("")); - assertThat(underTest.doGet("token", server.url("/"), Function.identity())) - .isEmpty(); + .setBody("")); + assertThat(underTest.doGet("token", server.url("/"), Function.identity())) + .isEmpty(); } @Test @@ -365,15 +380,17 @@ public class BitbucketServerRestClientTest { server.enqueue(new MockResponse() .setHeader("Content-Type", "application/json;charset=UTF-8") .setResponseCode(400) - .setBody("{\n" + - " \"errors\": [\n" + - " {\n" + - " \"context\": null,\n" + - " \"message\": \"Bad message\",\n" + - " \"exceptionName\": \"com.atlassian.bitbucket.auth.BadException\"\n" + - " }\n" + - " ]\n" + - "}")); + .setBody(""" + { + "errors": [ + { + "context": null, + "message": "Bad message", + "exceptionName": "com.atlassian.bitbucket.auth.BadException" + } + ] + } + """)); String serverUrl = server.url("/").toString(); assertThatThrownBy(() -> underTest.getRepo(serverUrl, "token", "", "")) @@ -386,15 +403,17 @@ public class BitbucketServerRestClientTest { server.enqueue(new MockResponse() .setHeader("Content-Type", "application/json;charset=UTF-8") .setResponseCode(401) - .setBody("{\n" + - " \"errors\": [\n" + - " {\n" + - " \"context\": null,\n" + - " \"message\": \"Bad message\",\n" + - " \"exceptionName\": \"com.atlassian.bitbucket.auth.BadException\"\n" + - " }\n" + - " ]\n" + - "}")); + .setBody(""" + { + "errors": [ + { + "context": null, + "message": "Bad message", + "exceptionName": "com.atlassian.bitbucket.auth.BadException" + } + ] + } + """)); String serverUrl = server.url("/").toString(); assertThatThrownBy(() -> underTest.getRepo(serverUrl, "token", "", "")) @@ -411,7 +430,7 @@ public class BitbucketServerRestClientTest { {401, "<p>unauthorized</p>", "application/json", "Invalid personal access token"}, {401, "<not-authorized>401</not-authorized>", "application/xhtml+xml", "Invalid personal access token"}, {403, "<p>forbidden</p>", "application/json;charset=UTF-8", "Unable to contact Bitbucket server"}, - {404, "<p>not found</p>","application/json;charset=UTF-8", "Error 404. The requested Bitbucket server is unreachable."}, + {404, "<p>not found</p>", "application/json;charset=UTF-8", "Error 404. The requested Bitbucket server is unreachable."}, {406, "<p>not accepted</p>", "application/json;charset=UTF-8", "Unable to contact Bitbucket server"}, {409, "<p>conflict</p>", "application/json;charset=UTF-8", "Unable to contact Bitbucket server"} }; @@ -421,14 +440,14 @@ public class BitbucketServerRestClientTest { @UseDataProvider("expectedErrorMessageFromHttpNoJsonBody") public void fail_response_when_http_no_json_body(int responseCode, String body, String headerContent, String expectedErrorMessage) { server.enqueue(new MockResponse() - .setHeader("Content-Type", headerContent) - .setResponseCode(responseCode) - .setBody(body)); + .setHeader("Content-Type", headerContent) + .setResponseCode(responseCode) + .setBody(body)); String serverUrl = server.url("/").toString(); assertThatThrownBy(() -> underTest.getRepo(serverUrl, "token", "", "")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(expectedErrorMessage); + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(expectedErrorMessage); } @Test @@ -490,46 +509,48 @@ public class BitbucketServerRestClientTest { @Test public void validate_token_success() { server.enqueue(new MockResponse().setResponseCode(200) - .setBody("{\n" + - " \"size\":10,\n" + - " \"limit\":25,\n" + - " \"isLastPage\":true,\n" + - " \"values\":[\n" + - " {\n" + - " \"name\":\"jean.michel\",\n" + - " \"emailAddress\":\"jean.michel@sonarsource.com\",\n" + - " \"id\":2,\n" + - " \"displayName\":\"Jean Michel\",\n" + - " \"active\":true,\n" + - " \"slug\":\"jean.michel\",\n" + - " \"type\":\"NORMAL\",\n" + - " \"links\":{\n" + - " \"self\":[\n" + - " {\n" + - " \"href\":\"https://bitbucket-testing.valiantys.sonarsource.com/users/jean.michel\"\n" + - " }\n" + - " ]\n" + - " }\n" + - " },\n" + - " {\n" + - " \"name\":\"prince.de.lu\",\n" + - " \"emailAddress\":\"prince.de.lu@sonarsource.com\",\n" + - " \"id\":103,\n" + - " \"displayName\":\"Prince de Lu\",\n" + - " \"active\":true,\n" + - " \"slug\":\"prince.de.lu\",\n" + - " \"type\":\"NORMAL\",\n" + - " \"links\":{\n" + - " \"self\":[\n" + - " {\n" + - " \"href\":\"https://bitbucket-testing.valiantys.sonarsource.com/users/prince.de.lu\"\n" + - " }\n" + - " ]\n" + - " }\n" + - " },\n" + - " ],\n" + - " \"start\":0\n" + - "}")); + .setBody(""" + { + "size":10, + "limit":25, + "isLastPage":true, + "values":[ + { + "name":"jean.michel", + "emailAddress":"jean.michel@sonarsource.com", + "id":2, + "displayName":"Jean Michel", + "active":true, + "slug":"jean.michel", + "type":"NORMAL", + "links":{ + "self":[ + { + "href":"https://bitbucket-testing.valiantys.sonarsource.com/users/jean.michel" + } + ] + } + }, + { + "name":"prince.de.lu", + "emailAddress":"prince.de.lu@sonarsource.com", + "id":103, + "displayName":"Prince de Lu", + "active":true, + "slug":"prince.de.lu", + "type":"NORMAL", + "links":{ + "self":[ + { + "href":"https://bitbucket-testing.valiantys.sonarsource.com/users/prince.de.lu" + } + ] + } + }, + ], + "start":0 + } + """)); underTest.validateToken(server.url("/").toString(), "token"); } @@ -621,7 +642,7 @@ public class BitbucketServerRestClientTest { .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unexpected response from Bitbucket server"); assertThat(String.join(", ", logTester.logs())) - .contains("Unexpected response from Bitbucket server : [this is not a json payload]"); + .contains("Unexpected response from Bitbucket server : [this is not a json payload]"); } @Test @@ -656,7 +677,7 @@ public class BitbucketServerRestClientTest { .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unexpected response from Bitbucket server"); assertThat(String.join(", ", logTester.logs())) - .contains("Unexpected response from Bitbucket server : [this is not a json payload]"); + .contains("Unexpected response from Bitbucket server : [this is not a json payload]"); } @Test @@ -672,11 +693,72 @@ public class BitbucketServerRestClientTest { @Test public void check_mediaTypes_equality() { - assertThat(underTest.equals(null, null)).isFalse(); - assertThat(underTest.equals(MediaType.parse("application/json"), null)).isFalse(); - assertThat(underTest.equals(null, MediaType.parse("application/json"))).isFalse(); - assertThat(underTest.equals(MediaType.parse("application/ json"), MediaType.parse("text/html; charset=UTF-8"))).isFalse(); - assertThat(underTest.equals(MediaType.parse("application/Json"), MediaType.parse("application/JSON"))).isTrue(); + assertThat(BitbucketServerRestClient.equals(null, null)).isFalse(); + assertThat(BitbucketServerRestClient.equals(MediaType.parse("application/json"), null)).isFalse(); + assertThat(BitbucketServerRestClient.equals(null, MediaType.parse("application/json"))).isFalse(); + assertThat(BitbucketServerRestClient.equals(MediaType.parse("application/ json"), MediaType.parse("text/html; charset=UTF-8"))).isFalse(); + assertThat(BitbucketServerRestClient.equals(MediaType.parse("application/Json"), MediaType.parse("application/JSON"))).isTrue(); + } + + @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() + .setHeader("Content-Type", "application/json;charset=UTF-8") + .setBody(body)); + + ProjectList gsonBBSProjectList = underTest.getProjects(server.url("/").toString(), "token", start, pageSize); + + int expectedSize = Math.min(pageSize, totalProjects - start + 1); + assertThat(gsonBBSProjectList.getValues()).hasSize(expectedSize); + + // Verify that the correct items are returned + IntStream.rangeClosed(start, start + expectedSize - 1).forEach(i -> { + assertThat(gsonBBSProjectList.getValues()) + .extracting(Project::getId, Project::getKey, Project::getName) + .contains(tuple((long) i, "KEY_" + i, "Project_" + i)); + }); + + assertThat(gsonBBSProjectList.isLastPage()).isEqualTo(isLastPage); + assertThat(gsonBBSProjectList.getNextPageStart()).isEqualTo(isLastPage ? start : start + expectedSize); + } + + private String generateProjectsResponse(int totalProjects, int start, int pageSize, boolean isLastPage) { + int end = Math.min(totalProjects, start + pageSize - 1); + + StringBuilder values = new StringBuilder(); + for (int i = start; i <= end; i++) { + values.append(""" + { + "key": "KEY_%d", + "id": %d, + "name": "Project_%d" + } + """.formatted(i, i, i)); + if (i < end) { + values.append(","); + } + } + + return """ + { + "isLastPage": %s, + "nextPageStart": %s, + "values": [%s] + } + """.formatted(isLastPage, isLastPage ? start : end + 1, values.toString()); + } + + @DataProvider + public static Object[][] validStartAndPageSizeProvider() { + return new Object[][] { + {1, 25, false, 50}, // First 25 items, not last + {26, 25, true, 50}, // Remaining items + {1, 10, false, 15}, // 10 items, more remaining + {11, 10, true, 15}, // Last 5 items + {21, 10, false, 100}, // Middle range of items + }; } } 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 7de69323b0b..8c704a69d49 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 @@ -19,8 +19,8 @@ */ package org.sonar.server.almintegration.ws.bitbucketserver; -import java.util.ArrayList; import java.util.List; +import java.util.stream.IntStream; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -29,7 +29,6 @@ import org.sonar.alm.client.bitbucketserver.Project; import org.sonar.alm.client.bitbucketserver.ProjectList; 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.user.UserDto; import org.sonar.server.exceptions.ForbiddenException; @@ -44,6 +43,8 @@ import org.sonarqube.ws.AlmIntegrations.ListBitbucketserverProjectsWsResponse; 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; @@ -60,85 +61,161 @@ public class ListBitbucketServerProjectsActionIT { private final BitbucketServerRestClient bitbucketServerRestClient = mock(BitbucketServerRestClient.class); private final WsActionTester ws = new WsActionTester(new ListBitbucketServerProjectsAction(db.getDbClient(), userSession, bitbucketServerRestClient)); + private static final String PARAM_ALM_SETTING = "almSetting"; + 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 totalMockedProjects = 100; + @Before public void before() { - ProjectList projectList = new ProjectList(); - List<Project> values = new ArrayList<>(); - Project project = new Project(); - project.setId(1); - project.setKey("key"); - project.setName("name"); - values.add(project); - projectList.setValues(values); - when(bitbucketServerRestClient.getProjects(anyString(), anyString())).thenReturn(projectList); + when(bitbucketServerRestClient.getProjects(anyString(), anyString(), any(), anyInt())) + .thenAnswer(invocation -> { + Integer start = invocation.getArgument(2); // Nullable + int pageSize = invocation.getArgument(3); + start = (start == null) ? DEFAULT_START : start; + return createTestProjectList(totalMockedProjects, start, pageSize); + }); } @Test - public void list_projects() { - UserDto user = db.users().insertUser(); - userSession.logIn(user).addPermission(PROVISION_PROJECTS); - AlmSettingDto almSetting = db.almSettings().insertGitHubAlmSetting(); - db.almPats().insert(dto -> { - dto.setAlmSettingUuid(almSetting.getUuid()); - dto.setUserUuid(user.getUuid()); - }); + public void handle_should_list_projects_when_default_pagination_parameters() { + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); ListBitbucketserverProjectsWsResponse response = ws.newRequest() - .setParam("almSetting", almSetting.getKey()) + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) .executeProtobuf(ListBitbucketserverProjectsWsResponse.class); - assertThat(response.getProjectsCount()).isOne(); - assertThat(response.getProjectsList()) - .extracting(AlmIntegrations.AlmProject::getKey, AlmIntegrations.AlmProject::getName) - .containsExactly(tuple("key", "name")); + validateResponse(response, DEFAULT_START, DEFAULT_PAGE_SIZE, false, DEFAULT_PAGE_SIZE + DEFAULT_START); } @Test - public void check_pat_is_missing() { - UserDto user = db.users().insertUser(); - userSession.logIn(user).addPermission(PROVISION_PROJECTS); - AlmSettingDto almSetting = db.almSettings().insertBitbucketAlmSetting(); + public void handle_should_list_projects_when_custom_start() { + totalMockedProjects = 55; + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); + + ListBitbucketserverProjectsWsResponse response = ws.newRequest() + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) + .setParam(PARAM_START, "51") + .executeProtobuf(ListBitbucketserverProjectsWsResponse.class); + + validateResponse(response, 51, 5, true, 51); + } + + @Test + public void handle_should_list_projects_when_custom_page_size() { + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); + + ListBitbucketserverProjectsWsResponse response = ws.newRequest() + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) + .setParam(PARAM_PAGE_SIZE, "10") + .executeProtobuf(ListBitbucketserverProjectsWsResponse.class); + + validateResponse(response, DEFAULT_START, 10, false, 10 + DEFAULT_START); + } + + @Test + public void handle_should_return_empty_list_when_out_of_bounds_start() { + totalMockedProjects = 30; + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); + + ListBitbucketserverProjectsWsResponse response = ws.newRequest() + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) + .setParam(PARAM_START, "50") // Out-of-bounds start + .setParam(PARAM_PAGE_SIZE, "10") + .executeProtobuf(ListBitbucketserverProjectsWsResponse.class); + + validateResponse(response, 50, 0, true, 50); + } + + @Test + public void handle_should_throw_illegal_argument_exception_when_invalid_start() { + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); TestRequest request = ws.newRequest() - .setParam("almSetting", almSetting.getKey()); + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) + .setParam(PARAM_START, "notAnInt"); assertThatThrownBy(request::execute) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("No personal access token found"); + .hasMessageContaining("The '%s' parameter cannot be parsed as an integer value: %s".formatted(PARAM_START, "notAnInt")); } @Test - public void fail_check_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); + 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("almSetting", "testKey"); + .setParam(PARAM_ALM_SETTING, almSetting.getKey()) + .setParam(PARAM_PAGE_SIZE, String.valueOf(MAX_PAGE_SIZE + 1)); assertThatThrownBy(request::execute) - .isInstanceOf(NotFoundException.class) - .hasMessage("DevOps Platform Setting 'testKey' not found"); + .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 fail_when_not_logged_in() { + public void handle_should_throw_illegal_argument_exception_when_invalid_page_size() { + UserDto user = createUserWithPermissions(); + AlmSettingDto almSetting = createAlmSettingWithPat(user); + TestRequest request = ws.newRequest() - .setParam("almSetting", "anyvalue"); + .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(request::execute) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("No personal access token found"); + } + + @Test + 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 '%s' not found".formatted("testKey")); + } + + @Test + 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); - TestRequest request = ws.newRequest() - .setParam("almSetting", "anyvalue"); + TestRequest request = ws.newRequest().setParam(PARAM_ALM_SETTING, "any"); assertThatThrownBy(request::execute) .isInstanceOf(ForbiddenException.class) @@ -146,7 +223,7 @@ public class ListBitbucketServerProjectsActionIT { } @Test - public void definition() { + public void definition_should_be_documented() { WebService.Action def = ws.getDef(); assertThat(def.since()).isEqualTo("8.2"); @@ -154,7 +231,61 @@ public class ListBitbucketServerProjectsActionIT { assertThat(def.responseExampleFormat()).isEqualTo("json"); assertThat(def.params()) .extracting(WebService.Param::key, WebService.Param::isRequired) - .containsExactlyInAnyOrder(tuple("almSetting", true)); + .containsExactlyInAnyOrder( + tuple(PARAM_ALM_SETTING, true), + tuple(PARAM_START, false), + tuple(PARAM_PAGE_SIZE, false)); + } + + // region utility methods + + private UserDto createUserWithPermissions() { + UserDto user = db.users().insertUser(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + return user; + } + + 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 ProjectList createTestProjectList(int nbProjects, int start, int pageSize) { + int end = Math.min(start + pageSize - 1, nbProjects); + + List<Project> projects = IntStream.rangeClosed(start, end) + .mapToObj(this::createTestProject) + .toList(); + + boolean isLastPage = end == nbProjects; + int nextPageStart = isLastPage ? start : end + 1; + + return new ProjectList(isLastPage, nextPageStart, projects.size(), projects); + } + + private Project createTestProject(int id) { + return new Project("project-key-" + id, "project-name-" + id, id); } + private void validateResponse(ListBitbucketserverProjectsWsResponse response, int start, int pageSize, boolean lastPage, int nextPageStart) { + int expectedProjectCount = Math.max(0, Math.min(pageSize, totalMockedProjects - start + 1)); + + assertThat(response.getPaging().getPageSize()).isEqualTo(expectedProjectCount); + assertThat(response.getIsLastPage()).isEqualTo(lastPage); + assertThat(response.getNextPageStart()).isEqualTo(nextPageStart); + assertThat(response.getProjectsCount()).isEqualTo(expectedProjectCount); + + assertThat(response.getProjectsList()) + .extracting(AlmIntegrations.AlmProject::getKey, AlmIntegrations.AlmProject::getName) + .containsExactlyElementsOf(IntStream.rangeClosed(start, start + expectedProjectCount - 1) + .mapToObj(id -> tuple("project-key-" + id, "project-name-" + id)) + .toList()); + } + + // endregion utility methods + } diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/ws/ComponentTreeActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/ws/ComponentTreeActionIT.java index 007dd505f68..0fa15ae84a1 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/ws/ComponentTreeActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/ws/ComponentTreeActionIT.java @@ -25,11 +25,8 @@ import java.util.stream.IntStream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.sonar.api.measures.Metric; -import org.sonar.server.component.ComponentTypeTree; -import org.sonar.server.component.ComponentTypes; import org.sonar.api.server.ws.WebService.Param; import org.sonar.api.utils.System2; -import org.sonar.server.component.DefaultComponentTypes; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.DbTester; @@ -37,12 +34,15 @@ import org.sonar.db.component.BranchDto; import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ComponentTesting; import org.sonar.db.component.ProjectData; -import org.sonar.server.component.ComponentTypesRule; import org.sonar.db.component.SnapshotDto; import org.sonar.db.measure.MeasureDto; import org.sonar.db.metric.MetricDto; import org.sonar.db.metric.MetricTesting; import org.sonar.server.component.ComponentFinder; +import org.sonar.server.component.ComponentTypeTree; +import org.sonar.server.component.ComponentTypes; +import org.sonar.server.component.ComponentTypesRule; +import org.sonar.server.component.DefaultComponentTypes; import org.sonar.server.exceptions.BadRequestException; import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.exceptions.NotFoundException; @@ -71,16 +71,15 @@ import static org.sonar.api.measures.Metric.ValueType.DISTRIB; import static org.sonar.api.measures.Metric.ValueType.FLOAT; import static org.sonar.api.measures.Metric.ValueType.INT; import static org.sonar.api.measures.Metric.ValueType.RATING; -import static org.sonar.db.component.ComponentQualifiers.APP; -import static org.sonar.db.component.ComponentQualifiers.DIRECTORY; -import static org.sonar.db.component.ComponentQualifiers.FILE; -import static org.sonar.db.component.ComponentQualifiers.UNIT_TEST_FILE; import static org.sonar.api.server.ws.WebService.Param.SORT; import static org.sonar.api.utils.DateUtils.parseDateTime; import static org.sonar.api.web.UserRole.USER; import static org.sonar.db.component.BranchType.PULL_REQUEST; import static org.sonar.db.component.ComponentDbTester.toProjectDto; -import static org.sonar.db.component.ComponentTesting.newBranchDto; +import static org.sonar.db.component.ComponentQualifiers.APP; +import static org.sonar.db.component.ComponentQualifiers.DIRECTORY; +import static org.sonar.db.component.ComponentQualifiers.FILE; +import static org.sonar.db.component.ComponentQualifiers.UNIT_TEST_FILE; import static org.sonar.db.component.ComponentTesting.newDirectory; import static org.sonar.db.component.ComponentTesting.newFileDto; import static org.sonar.db.component.ComponentTesting.newProjectCopy; @@ -113,7 +112,7 @@ class ComponentTreeActionIT { private final I18nRule i18n = new I18nRule(); - private final ComponentTypes defaultComponentTypes = new ComponentTypes(new ComponentTypeTree[]{DefaultComponentTypes.get()}); + private final ComponentTypes defaultComponentTypes = new ComponentTypes(new ComponentTypeTree[] {DefaultComponentTypes.get()}); private final ComponentTypesRule resourceTypes = new ComponentTypesRule() .setRootQualifiers(defaultComponentTypes.getRoots()) .setAllQualifiers(defaultComponentTypes.getAll()) @@ -135,8 +134,8 @@ class ComponentTreeActionIT { addProjectPermission(projectData); db.components().insertSnapshot(mainBranch, s -> s.setPeriodDate(parseDateTime("2016-01-11T10:49:50+0100").getTime()) - .setPeriodMode("previous_version") - .setPeriodParam("1.0-SNAPSHOT")); + .setPeriodMode("previous_version") + .setPeriodParam("1.0-SNAPSHOT")); ComponentDto file1 = db.components().insertComponent(newFileDto(mainBranch) .setUuid("AVIwDXE-bJbJqrw6wFv5") .setKey("com.sonarsource:java-markdown:src/main/java/com/sonarsource/markdown/impl/ElementImpl.java") @@ -201,8 +200,8 @@ class ComponentTreeActionIT { ComponentDto mainBranch = projectData.getMainBranchComponent(); db.components().insertSnapshot(mainBranch, s -> s.setPeriodDate(parseDateTime("2016-01-11T10:49:50+0100").getTime()) - .setPeriodMode("previous_version") - .setPeriodParam("1.0-SNAPSHOT")); + .setPeriodMode("previous_version") + .setPeriodParam("1.0-SNAPSHOT")); MetricDto accepted_issues = insertAcceptedIssuesMetric(); db.measures().insertMeasure(mainBranch, m -> m.addValue(accepted_issues.getKey(), 10d)); @@ -438,24 +437,15 @@ class ComponentTreeActionIT { ComponentDto mainBranch = projectData.getMainBranchComponent(); addProjectPermission(projectData); db.components().insertSnapshot(mainBranch); - ComponentDto file9 = db.components().insertComponent(newFileDto(mainBranch, null, "file-uuid-9").setName("file-1").setKey("file-9-key" - )); - ComponentDto file8 = db.components().insertComponent(newFileDto(mainBranch, null, "file-uuid-8").setName("file-1").setKey("file-8-key" - )); - ComponentDto file7 = db.components().insertComponent(newFileDto(mainBranch, null, "file-uuid-7").setName("file-1").setKey("file-7-key" - )); - ComponentDto file6 = db.components().insertComponent(newFileDto(mainBranch, null, "file-uuid-6").setName("file-1").setKey("file-6-key" - )); - ComponentDto file5 = db.components().insertComponent(newFileDto(mainBranch, null, "file-uuid-5").setName("file-1").setKey("file-5-key" - )); - ComponentDto file4 = db.components().insertComponent(newFileDto(mainBranch, null, "file-uuid-4").setName("file-1").setKey("file-4-key" - )); - ComponentDto file3 = db.components().insertComponent(newFileDto(mainBranch, null, "file-uuid-3").setName("file-1").setKey("file-3-key" - )); - ComponentDto file2 = db.components().insertComponent(newFileDto(mainBranch, null, "file-uuid-2").setName("file-1").setKey("file-2-key" - )); - ComponentDto file1 = db.components().insertComponent(newFileDto(mainBranch, null, "file-uuid-1").setName("file-1").setKey("file-1-key" - )); + ComponentDto file9 = db.components().insertComponent(newFileDto(mainBranch, null, "file-uuid-9").setName("file-1").setKey("file-9-key")); + ComponentDto file8 = db.components().insertComponent(newFileDto(mainBranch, null, "file-uuid-8").setName("file-1").setKey("file-8-key")); + ComponentDto file7 = db.components().insertComponent(newFileDto(mainBranch, null, "file-uuid-7").setName("file-1").setKey("file-7-key")); + ComponentDto file6 = db.components().insertComponent(newFileDto(mainBranch, null, "file-uuid-6").setName("file-1").setKey("file-6-key")); + ComponentDto file5 = db.components().insertComponent(newFileDto(mainBranch, null, "file-uuid-5").setName("file-1").setKey("file-5-key")); + ComponentDto file4 = db.components().insertComponent(newFileDto(mainBranch, null, "file-uuid-4").setName("file-1").setKey("file-4-key")); + ComponentDto file3 = db.components().insertComponent(newFileDto(mainBranch, null, "file-uuid-3").setName("file-1").setKey("file-3-key")); + ComponentDto file2 = db.components().insertComponent(newFileDto(mainBranch, null, "file-uuid-2").setName("file-1").setKey("file-2-key")); + ComponentDto file1 = db.components().insertComponent(newFileDto(mainBranch, null, "file-uuid-1").setName("file-1").setKey("file-1-key")); MetricDto coverage = insertCoverageMetric(); db.commit(); db.measures().insertMeasure(file1, m -> m.addValue(coverage.getKey(), 1.0d)); @@ -962,8 +952,7 @@ class ComponentTreeActionIT { .setParam(PARAM_METRIC_KEYS, format("%s,%s,%s", NEW_SECURITY_ISSUES_KEY, NEW_MAINTAINABILITY_ISSUES_KEY, - NEW_RELIABILITY_ISSUES_KEY - )) + NEW_RELIABILITY_ISSUES_KEY)) .setParam(PARAM_ADDITIONAL_FIELDS, "metrics,period") .executeProtobuf(ComponentTreeWsResponse.class); @@ -975,8 +964,7 @@ class ComponentTreeActionIT { .extracting(Measure::getMetric, m -> m.getPeriod().getValue()) .containsExactlyInAnyOrder(tuple(NEW_SECURITY_ISSUES_KEY, NEW_SECURITY_ISSUES_KEY + "_data"), tuple(NEW_MAINTAINABILITY_ISSUES_KEY, NEW_MAINTAINABILITY_ISSUES_KEY + "_data"), - tuple(NEW_RELIABILITY_ISSUES_KEY, NEW_RELIABILITY_ISSUES_KEY + "_data") - ); + tuple(NEW_RELIABILITY_ISSUES_KEY, NEW_RELIABILITY_ISSUES_KEY + "_data")); } @Test @@ -995,7 +983,7 @@ class ComponentTreeActionIT { .execute(); }) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("'metricKeys' can contains only 25 values, got 26"); + .hasMessage("'metricKeys' can contain only 25 values, got 26"); } @Test diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/permission/ws/template/BulkApplyTemplateActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/permission/ws/template/BulkApplyTemplateActionIT.java index fba9ef2cd46..b69b3e8486e 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/permission/ws/template/BulkApplyTemplateActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/permission/ws/template/BulkApplyTemplateActionIT.java @@ -24,14 +24,13 @@ import java.util.List; import org.apache.commons.lang3.StringUtils; import org.junit.Before; import org.junit.Test; -import org.sonar.db.component.ComponentQualifiers; import org.sonar.api.server.ws.WebService.Param; import org.sonar.api.web.UserRole; import org.sonar.core.util.SequenceUuidFactory; import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentQualifiers; import org.sonar.db.component.ComponentTesting; import org.sonar.db.component.ProjectData; -import org.sonar.server.component.ComponentTypesRule; import org.sonar.db.entity.EntityDto; import org.sonar.db.permission.PermissionQuery; import org.sonar.db.permission.template.PermissionTemplateDto; @@ -39,15 +38,16 @@ import org.sonar.db.portfolio.PortfolioDto; import org.sonar.db.project.ProjectDto; import org.sonar.db.user.GroupDto; import org.sonar.db.user.UserDto; +import org.sonar.server.common.permission.DefaultTemplatesResolver; +import org.sonar.server.common.permission.DefaultTemplatesResolverImpl; +import org.sonar.server.common.permission.PermissionTemplateService; +import org.sonar.server.component.ComponentTypesRule; import org.sonar.server.es.Indexers; import org.sonar.server.es.TestIndexers; import org.sonar.server.exceptions.BadRequestException; import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.l18n.I18nRule; import org.sonar.server.management.ManagedProjectService; -import org.sonar.server.common.permission.DefaultTemplatesResolver; -import org.sonar.server.common.permission.DefaultTemplatesResolverImpl; -import org.sonar.server.common.permission.PermissionTemplateService; import org.sonar.server.permission.ws.BasePermissionWsIT; import static org.assertj.core.api.Assertions.assertThat; @@ -56,10 +56,10 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.sonar.api.utils.DateUtils.parseDate; import static org.sonar.db.component.ComponentQualifiers.APP; import static org.sonar.db.component.ComponentQualifiers.PROJECT; import static org.sonar.db.component.ComponentQualifiers.VIEW; -import static org.sonar.api.utils.DateUtils.parseDate; import static org.sonar.db.component.SnapshotTesting.newAnalysis; import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_TEMPLATE_ID; import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_TEMPLATE_NAME; @@ -150,7 +150,7 @@ public class BulkApplyTemplateActionIT extends BasePermissionWsIT<BulkApplyTempl .execute(); }) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("'projects' can contains only 1000 values, got 1001"); + .hasMessage("'projects' can contain only 1000 values, got 1001"); } @Test diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/project/ws/SearchActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/project/ws/SearchActionIT.java index 2fca3b9b03b..b529194eff7 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/project/ws/SearchActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/project/ws/SearchActionIT.java @@ -225,8 +225,7 @@ public class SearchActionIT { SearchWsResponse response = call(SearchRequest.builder().build()); assertThat(response.getComponentsList()).extracting(Component::getKey, Component::getLastAnalysisDate, Component::getRevision) .containsExactlyInAnyOrder( - tuple(project.projectKey(), formatDateTime(snapshotBranch.getCreatedAt()), snapshotProject.getRevision()) - ); + tuple(project.projectKey(), formatDateTime(snapshotBranch.getCreatedAt()), snapshotProject.getRevision())); } @Test @@ -261,8 +260,7 @@ public class SearchActionIT { when(managedProjectService.getProjectUuidToManaged(any(), eq(Set.of(managedProject.projectUuid(), notManagedProject.projectUuid())))) .thenReturn(Map.of( managedProject.projectUuid(), true, - notManagedProject.projectUuid(), false - )); + notManagedProject.projectUuid(), false)); SearchWsResponse result = call(SearchRequest.builder().build()); assertThat(result.getComponentsList()) @@ -337,7 +335,7 @@ public class SearchActionIT { .build(); assertThatThrownBy(() -> call(request)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("'projects' can contains only 1000 values, got 1001"); + .hasMessage("'projects' can contain only 1000 values, got 1001"); } @Test @@ -377,17 +375,23 @@ public class SearchActionIT { assertThat(action.responseExample()).isEqualTo(getClass().getResource("search-example.json")); var definition = ws.getDef(); - assertThat(definition.params()).extracting(WebService.Param::key, WebService.Param::isRequired, WebService.Param::description, WebService.Param::possibleValues, WebService.Param::defaultValue, WebService.Param::since) + assertThat(definition.params()) + .extracting(WebService.Param::key, WebService.Param::isRequired, WebService.Param::description, WebService.Param::possibleValues, WebService.Param::defaultValue, + WebService.Param::since) .containsExactlyInAnyOrder( - tuple("q", false, "Limit search to: <ul><li>component names that contain the supplied string</li><li>component keys that contain the supplied string</li></ul>", null, null, null), + tuple("q", false, "Limit search to: <ul><li>component names that contain the supplied string</li><li>component keys that contain the supplied string</li></ul>", null, null, + null), tuple("qualifiers", false, "Comma-separated list of component qualifiers. Filter the results with the specified qualifiers", Set.of("TRK", "VW", "APP"), "TRK", null), tuple("p", false, "1-based page number", null, "1", null), tuple("projects", false, "Comma-separated list of project keys", null, null, "6.6"), tuple("ps", false, "Page size. Must be greater than 0 and less or equal than 500", null, "100", null), - tuple("visibility", false, "Filter the projects that should be visible to everyone (public), or only specific user/groups (private).<br/>If no visibility is specified, the default project visibility will be used.", Set.of("private", "public"), null, "6.4"), - tuple("analyzedBefore", false, "Filter the projects for which the last analysis of all branches are older than the given date (exclusive).<br> Either a date (server timezone) or datetime can be provided.", null, null, "6.6"), - tuple("onProvisionedOnly", false, "Filter the projects that are provisioned", Set.of("true", "false", "yes", "no"), "false", "6.6") - ); + tuple("visibility", false, + "Filter the projects that should be visible to everyone (public), or only specific user/groups (private).<br/>If no visibility is specified, the default project visibility will be used.", + Set.of("private", "public"), null, "6.4"), + tuple("analyzedBefore", false, + "Filter the projects for which the last analysis of all branches are older than the given date (exclusive).<br> Either a date (server timezone) or datetime can be provided.", + null, null, "6.6"), + tuple("onProvisionedOnly", false, "Filter the projects that are provisioned", Set.of("true", "false", "yes", "no"), "false", "6.6")); } @Test 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 3b9b97020ff..0755702f3c0 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 @@ -36,6 +36,7 @@ import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.user.UserSession; import org.sonarqube.ws.AlmIntegrations.AlmProject; import org.sonarqube.ws.AlmIntegrations.ListBitbucketserverProjectsWsResponse; +import org.sonarqube.ws.Common; import static java.util.Objects.requireNonNull; import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; @@ -44,6 +45,10 @@ import static org.sonar.server.ws.WsUtils.writeProtobuf; public class ListBitbucketServerProjectsAction implements AlmIntegrationsWsAction { private static final String PARAM_ALM_SETTING = "almSetting"; + 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; @@ -69,6 +74,15 @@ public class ListBitbucketServerProjectsAction implements AlmIntegrationsWsActio .setRequired(true) .setMaximumLength(200) .setDescription("DevOps Platform setting key"); + + action.createParam(PARAM_START) + .setExampleValue(2154) + .setDescription("Start number for the page (inclusive). If not passed, first page is assumed."); + + action.createParam(PARAM_PAGE_SIZE) + .setDefaultValue(DEFAULT_PAGE_SIZE) + .setMaximumValue(MAX_PAGE_SIZE) + .setDescription("Number of items to return."); } @Override @@ -78,24 +92,31 @@ public class ListBitbucketServerProjectsAction implements AlmIntegrationsWsActio } private ListBitbucketserverProjectsWsResponse doHandle(Request request) { + userSession.checkLoggedIn().checkPermission(PROVISION_PROJECTS); - try (DbSession dbSession = dbClient.openSession(false)) { - 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)).orElse(DEFAULT_PAGE_SIZE); + String userUuid = requireNonNull(userSession.getUuid(), "User UUID is not null"); - String almSettingKey = request.mandatoryParam(PARAM_ALM_SETTING); - String userUuid = requireNonNull(userSession.getUuid(), "User UUID is not null"); + try (DbSession dbSession = dbClient.openSession(false)) { 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 pat = almPatDto.map(AlmPatDto::getPersonalAccessToken).orElseThrow(() -> new IllegalArgumentException("No personal access token found")); String url = requireNonNull(almSettingDto.getUrl(), "URL cannot be null"); - ProjectList projectList = bitbucketServerRestClient.getProjects(url, pat); - + ProjectList projectList = bitbucketServerRestClient.getProjects(url, pat, start, pageSize); List<AlmProject> values = projectList.getValues().stream().map(ListBitbucketServerProjectsAction::toAlmProject).toList(); - ListBitbucketserverProjectsWsResponse.Builder builder = ListBitbucketserverProjectsWsResponse.newBuilder() - .addAllProjects(values); - return builder.build(); + + return ListBitbucketserverProjectsWsResponse.newBuilder() + .setIsLastPage(projectList.isLastPage()) + .setNextPageStart(projectList.getNextPageStart()) + .setPaging(Common.Paging.newBuilder() + .setPageSize(projectList.getSize()) + .build()) + .addAllProjects(values) + .build(); } } |