aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClient.java20
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/ProjectList.java42
-rw-r--r--server/sonar-alm-client/src/test/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClientTest.java546
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/bitbucketserver/ListBitbucketServerProjectsActionIT.java221
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/ws/ComponentTreeActionIT.java62
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/permission/ws/template/BulkApplyTemplateActionIT.java14
-rw-r--r--server/sonar-webserver-webapi/src/it/java/org/sonar/server/project/ws/SearchActionIT.java26
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketserver/ListBitbucketServerProjectsAction.java39
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();
}
}