From 97baa10facb3a4e6295fd7dfe93359ebd578c60f Mon Sep 17 00:00:00 2001 From: Zipeng WU Date: Tue, 11 May 2021 22:11:39 +0200 Subject: [PATCH] SONAR-14802 Search bitbucket cloud repository for onboarding --- .../BitbucketCloudRestClient.java | 6 + .../bitbucket/bitbucketcloud/Project.java | 57 ++++++ .../bitbucket/bitbucketcloud/Repository.java | 65 +++++++ .../bitbucketcloud/RepositoryList.java | 66 +++++++ .../BitbucketCloudRestClientTest.java | 64 +++++++ .../ws/AlmIntegrationsWSModule.java | 2 + .../SearchBitbucketCloudReposAction.java | 140 ++++++++++++++ .../ws/bitbucketcloud/package-info.java | 23 +++ .../SearchBitbucketServerReposAction.java | 2 - .../SearchBitbucketCloudReposActionTest.java | 181 ++++++++++++++++++ .../main/protobuf/ws-alm_integrations.proto | 16 ++ 11 files changed, 620 insertions(+), 2 deletions(-) create mode 100644 server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/Project.java create mode 100644 server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/Repository.java create mode 100644 server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/RepositoryList.java create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketcloud/SearchBitbucketCloudReposAction.java create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketcloud/package-info.java create mode 100644 server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/bitbucketcloud/SearchBitbucketCloudReposActionTest.java diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/BitbucketCloudRestClient.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/BitbucketCloudRestClient.java index 34c7ba2572c..1627f75869c 100644 --- a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/BitbucketCloudRestClient.java +++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/BitbucketCloudRestClient.java @@ -134,6 +134,12 @@ public class BitbucketCloudRestClient { } } + public RepositoryList searchRepos(String encodedCredentials, String workspace, @Nullable String repoName, Integer page, Integer pageSize) { + String filterQuery = String.format("q=name~\"%s\"", repoName != null ? repoName : ""); + HttpUrl url = buildUrl(String.format("/repositories/%s?%s&page=%s&pagelen=%s", workspace, filterQuery, page, pageSize)); + return doGetWithBasicAuth(encodedCredentials, url, r -> buildGson().fromJson(r.body().charStream(), RepositoryList.class)); + } + public String createAccessToken(String clientId, String clientSecret) { Request request = createAccessTokenRequest(clientId, clientSecret); return doCall(request, r -> buildGson().fromJson(r.body().charStream(), Token.class)).getAccessToken(); diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/Project.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/Project.java new file mode 100644 index 00000000000..9e32c6774b4 --- /dev/null +++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/Project.java @@ -0,0 +1,57 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.alm.client.bitbucket.bitbucketcloud; + +import com.google.gson.annotations.SerializedName; + +public class Project { + + @SerializedName("key") + private String key; + + @SerializedName("name") + private String name; + + @SerializedName("uuid") + private String uuid; + + public Project() { + // http://stackoverflow.com/a/18645370/229031 + } + + public Project(String uuid, String key, String name) { + this.uuid = uuid; + this.key = key; + this.name = name; + } + + public String getKey() { + return key; + } + + public String getName() { + return name; + } + + public String getUuid() { + return uuid; + } + +} diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/Repository.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/Repository.java new file mode 100644 index 00000000000..534a4773aff --- /dev/null +++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/Repository.java @@ -0,0 +1,65 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.alm.client.bitbucket.bitbucketcloud; + +import com.google.gson.annotations.SerializedName; + +public class Repository { + + @SerializedName("slug") + private String slug; + + @SerializedName("name") + private String name; + + @SerializedName("uuid") + private String uuid; + + @SerializedName("project") + private Project project; + + public Repository() { + // http://stackoverflow.com/a/18645370/229031 + } + + public Repository(String uuid, String slug, String name, Project project) { + this.uuid = uuid; + this.slug = slug; + this.name = name; + this.project = project; + } + + public String getSlug() { + return slug; + } + + public String getName() { + return name; + } + + public String getUuid() { + return uuid; + } + + public Project getProject() { + return project; + } + +} diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/RepositoryList.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/RepositoryList.java new file mode 100644 index 00000000000..73cc74d6c9c --- /dev/null +++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/RepositoryList.java @@ -0,0 +1,66 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.alm.client.bitbucket.bitbucketcloud; + +import com.google.gson.annotations.SerializedName; +import java.util.List; + +public class RepositoryList { + + @SerializedName("next") + private String next; + + @SerializedName("page") + private Integer page; + + @SerializedName("pagelen") + private Integer pagelen; + + @SerializedName("values") + private List values; + + public RepositoryList() { + // http://stackoverflow.com/a/18645370/229031 + } + + public RepositoryList(String next, List values, Integer page, Integer pagelen) { + this.next = next; + this.values = values; + this.page = page; + this.pagelen = pagelen; + } + + public String getNext() { + return next; + } + + public List getValues() { + return values; + } + + public Integer getPage() { + return page; + } + + public Integer getPagelen() { + return pagelen; + } + +} diff --git a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/bitbucket/bitbucketcloud/BitbucketCloudRestClientTest.java b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/bitbucket/bitbucketcloud/BitbucketCloudRestClientTest.java index 387da13f89c..7a6d675a7c5 100644 --- a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/bitbucket/bitbucketcloud/BitbucketCloudRestClientTest.java +++ b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/bitbucket/bitbucketcloud/BitbucketCloudRestClientTest.java @@ -19,6 +19,7 @@ */ package org.sonar.alm.client.bitbucket.bitbucketcloud; +import com.google.gson.Gson; import java.io.IOException; import okhttp3.Call; import okhttp3.OkHttpClient; @@ -34,9 +35,11 @@ import org.junit.Before; import org.junit.Test; import org.sonarqube.ws.client.OkHttpClientBuilder; +import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -58,6 +61,67 @@ public class BitbucketCloudRestClientTest { server.shutdown(); } + @Test + public void get_repos() { + server.enqueue(new MockResponse() + .setHeader("Content-Type", "application/json;charset=UTF-8") + .setBody("{\n" + + " \"values\": [\n" + + " {\n" + + " \"slug\": \"banana\",\n" + + " \"uuid\": \"BANANA-UUID\",\n" + + " \"name\": \"banana\",\n" + + " \"project\": {\n" + + " \"key\": \"HOY\",\n" + + " \"uuid\": \"BANANA-PROJECT-UUID\",\n" + + " \"name\": \"hoy\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"slug\": \"potato\",\n" + + " \"uuid\": \"POTATO-UUID\",\n" + + " \"name\": \"potato\",\n" + + " \"project\": {\n" + + " \"key\": \"HEY\",\n" + + " \"uuid\": \"POTATO-PROJECT-UUID\",\n" + + " \"name\": \"hey\"\n" + + " }\n" + + " }\n" + + " ]\n" + + "}")); + + RepositoryList repositoryList = underTest.searchRepos("user:apppwd", "", null, 1, 100); + assertThat(repositoryList.getNext()).isNull(); + assertThat(repositoryList.getValues()) + .hasSize(2) + .extracting(Repository::getUuid, Repository::getName, Repository::getSlug, + g -> g.getProject().getUuid(), g -> g.getProject().getKey(), g -> g.getProject().getName()) + .containsExactlyInAnyOrder( + tuple("BANANA-UUID", "banana", "banana", "BANANA-PROJECT-UUID", "HOY", "hoy"), + tuple("POTATO-UUID", "potato", "potato", "POTATO-PROJECT-UUID", "HEY", "hey")); + } + + @Test + public void bbc_object_serialization_deserialization() { + Project project = new Project("PROJECT-UUID-ONE", "projectKey", "projectName"); + Repository repository = new Repository("REPO-UUID-ONE", "repo-slug", "repoName", project); + RepositoryList repos = new RepositoryList(null, asList(repository), 1, 100); + server.enqueue(new MockResponse() + .setHeader("Content-Type", "application/json;charset=UTF-8") + .setBody(new Gson().toJson(repos))); + + RepositoryList repositoryList = underTest.searchRepos("user:apppwd", "", null, 1, 100); + assertThat(repositoryList.getNext()).isNull(); + assertThat(repositoryList.getPage()).isEqualTo(1); + assertThat(repositoryList.getPagelen()).isEqualTo(100); + assertThat(repositoryList.getValues()) + .hasSize(1) + .extracting(Repository::getUuid, Repository::getName, Repository::getSlug, + g -> g.getProject().getUuid(), g -> g.getProject().getKey(), g -> g.getProject().getName()) + .containsExactlyInAnyOrder( + tuple("REPO-UUID-ONE", "repoName", "repo-slug", "PROJECT-UUID-ONE", "projectKey", "projectName")); + } + @Test public void failIfUnauthorized() { server.enqueue(new MockResponse().setResponseCode(401).setBody("Unauthorized")); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModule.java index 01e1fa92172..776327931cb 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModule.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModule.java @@ -23,6 +23,7 @@ import org.sonar.core.platform.Module; import org.sonar.server.almintegration.ws.azure.ImportAzureProjectAction; import org.sonar.server.almintegration.ws.azure.ListAzureProjectsAction; import org.sonar.server.almintegration.ws.azure.SearchAzureReposAction; +import org.sonar.server.almintegration.ws.bitbucketcloud.SearchBitbucketCloudReposAction; import org.sonar.server.almintegration.ws.bitbucketserver.ImportBitbucketServerProjectAction; import org.sonar.server.almintegration.ws.bitbucketserver.ListBitbucketServerProjectsAction; import org.sonar.server.almintegration.ws.bitbucketserver.SearchBitbucketServerReposAction; @@ -42,6 +43,7 @@ public class AlmIntegrationsWSModule extends Module { ImportBitbucketServerProjectAction.class, ListBitbucketServerProjectsAction.class, SearchBitbucketServerReposAction.class, + SearchBitbucketCloudReposAction.class, GetGithubClientIdAction.class, ImportGithubProjectAction.class, ListGithubOrganizationsAction.class, diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketcloud/SearchBitbucketCloudReposAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketcloud/SearchBitbucketCloudReposAction.java new file mode 100644 index 00000000000..15267d12951 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketcloud/SearchBitbucketCloudReposAction.java @@ -0,0 +1,140 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.almintegration.ws.bitbucketcloud; + +import java.util.List; +import java.util.Optional; +import org.sonar.alm.client.bitbucket.bitbucketcloud.BitbucketCloudRestClient; +import org.sonar.alm.client.bitbucket.bitbucketcloud.Repository; +import org.sonar.alm.client.bitbucket.bitbucketcloud.RepositoryList; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.pat.AlmPatDto; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.user.UserSession; +import org.sonarqube.ws.AlmIntegrations.BBCRepo; +import org.sonarqube.ws.AlmIntegrations.SearchBitbucketcloudReposWsResponse; +import org.sonarqube.ws.Common; + +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; +import static java.util.stream.Collectors.toList; +import static org.sonar.api.server.ws.WebService.Param.PAGE; +import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE; +import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; +import static org.sonar.server.ws.WsUtils.writeProtobuf; + +public class SearchBitbucketCloudReposAction implements AlmIntegrationsWsAction { + + private static final String PARAM_ALM_SETTING = "almSetting"; + private static final String PARAM_REPO_NAME = "repositoryName"; + private static final int DEFAULT_PAGE_SIZE = 20; + private static final int MAX_PAGE_SIZE = 100; + + private final DbClient dbClient; + private final UserSession userSession; + private final BitbucketCloudRestClient bitbucketCloudRestClient; + + public SearchBitbucketCloudReposAction(DbClient dbClient, UserSession userSession, + BitbucketCloudRestClient bitbucketCloudRestClient) { + this.dbClient = dbClient; + this.userSession = userSession; + this.bitbucketCloudRestClient = bitbucketCloudRestClient; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("search_bitbucketcloud_repos") + .setDescription("Search the Bitbucket Cloud repositories
" + + "Requires the 'Create Projects' permission") + .setPost(false) + .setSince("9.0") + .setHandler(this); + + action.createParam(PARAM_ALM_SETTING) + .setRequired(true) + .setMaximumLength(200) + .setDescription("ALM setting key"); + + action.createParam(PARAM_REPO_NAME) + .setRequired(false) + .setMaximumLength(200) + .setDescription("Repository name filter"); + + action.addPagingParams(DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE); + } + + @Override + public void handle(Request request, Response response) { + SearchBitbucketcloudReposWsResponse wsResponse = doHandle(request); + writeProtobuf(wsResponse, request, response); + } + + private SearchBitbucketcloudReposWsResponse doHandle(Request request) { + userSession.checkLoggedIn().checkPermission(PROVISION_PROJECTS); + String almSettingKey = request.mandatoryParam(PARAM_ALM_SETTING); + String repoName = request.param(PARAM_REPO_NAME); + int page = request.mandatoryParamAsInt(PAGE); + int pageSize = request.mandatoryParamAsInt(PAGE_SIZE); + + try (DbSession dbSession = dbClient.openSession(false)) { + AlmSettingDto almSettingDto = dbClient.almSettingDao().selectByKey(dbSession, almSettingKey) + .orElseThrow(() -> new NotFoundException(String.format("ALM Setting '%s' not found", almSettingKey))); + + String workspace = ofNullable(almSettingDto.getAppId()) + .orElseThrow(() -> new IllegalArgumentException(String.format("workspace for alm setting %s is missing", almSettingDto.getKey()))); + String userUuid = requireNonNull(userSession.getUuid(), "User UUID cannot be null"); + Optional almPatDto = dbClient.almPatDao().selectByUserAndAlmSetting(dbSession, userUuid, almSettingDto); + + String pat = almPatDto.map(AlmPatDto::getPersonalAccessToken).orElseThrow(() -> new IllegalArgumentException("No personal access token found")); + + RepositoryList repositoryList = bitbucketCloudRestClient.searchRepos(pat, workspace, repoName, page, pageSize); + + List bbcRepos = repositoryList.getValues().stream() + .map(repository -> toBBCRepo(repository, workspace)) + .collect(toList()); + + SearchBitbucketcloudReposWsResponse.Builder builder = SearchBitbucketcloudReposWsResponse.newBuilder() + .setIsLastPage(repositoryList.getNext() == null) + .setPaging(Common.Paging.newBuilder() + .setPageIndex(page) + .setPageSize(pageSize) + .build()) + .addAllRepositories(bbcRepos); + return builder.build(); + } + } + + private static BBCRepo toBBCRepo(Repository gsonBBCRepo, String workspace) { + return BBCRepo.newBuilder() + .setSlug(gsonBBCRepo.getSlug()) + .setUuid(gsonBBCRepo.getUuid()) + .setName(gsonBBCRepo.getName()) + .setWorkspace(workspace) + .setProjectKey(gsonBBCRepo.getProject().getKey()) + .build(); + } + +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketcloud/package-info.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketcloud/package-info.java new file mode 100644 index 00000000000..f9398c22ced --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketcloud/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.almintegration.ws.bitbucketcloud; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketserver/SearchBitbucketServerReposAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketserver/SearchBitbucketServerReposAction.java index aa009e10c3c..f9a295db803 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketserver/SearchBitbucketServerReposAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketserver/SearchBitbucketServerReposAction.java @@ -125,8 +125,6 @@ public class SearchBitbucketServerReposAction implements AlmIntegrationsWsAction String url = requireNonNull(almSettingDto.getUrl(), "ALM url cannot be null"); RepositoryList gsonBBSRepoList = bitbucketServerRestClient.getRepos(url, pat, projectKey, repoName); - LOG.info(gsonBBSRepoList.toString()); - Map sqProjectsKeyByBBSKey = getSqProjectsKeyByBBSKey(dbSession, almSettingDto, gsonBBSRepoList); List bbsRepos = gsonBBSRepoList.getValues().stream().map(gsonBBSRepo -> toBBSRepo(gsonBBSRepo, sqProjectsKeyByBBSKey)) .collect(toList()); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/bitbucketcloud/SearchBitbucketCloudReposActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/bitbucketcloud/SearchBitbucketCloudReposActionTest.java new file mode 100644 index 00000000000..be24d49f87a --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/bitbucketcloud/SearchBitbucketCloudReposActionTest.java @@ -0,0 +1,181 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.almintegration.ws.bitbucketcloud; + +import org.jetbrains.annotations.NotNull; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.alm.client.bitbucket.bitbucketcloud.BitbucketCloudRestClient; +import org.sonar.alm.client.bitbucket.bitbucketcloud.Project; +import org.sonar.alm.client.bitbucket.bitbucketcloud.Repository; +import org.sonar.alm.client.bitbucket.bitbucketcloud.RepositoryList; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbTester; +import org.sonar.db.alm.pat.AlmPatDto; +import org.sonar.db.alm.setting.AlmSettingDto; +import org.sonar.db.project.ProjectDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.WsActionTester; +import org.sonarqube.ws.AlmIntegrations.BBCRepo; +import org.sonarqube.ws.AlmIntegrations.SearchBitbucketcloudReposWsResponse; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +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.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.api.server.ws.WebService.Param.PAGE; +import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE; +import static org.sonar.db.alm.integration.pat.AlmPatsTesting.newAlmPatDto; +import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; + +public class SearchBitbucketCloudReposActionTest { + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + @Rule + public DbTester db = DbTester.create(); + + private final BitbucketCloudRestClient bitbucketCloudRestClient = mock(BitbucketCloudRestClient.class); + + private final WsActionTester ws = new WsActionTester( + new SearchBitbucketCloudReposAction(db.getDbClient(), userSession, bitbucketCloudRestClient)); + + @Test + public void list_repos() { + when(bitbucketCloudRestClient.searchRepos(any(), any(), any(), any(), any())).thenReturn(getRepositoryList()); + UserDto user = db.users().insertUser(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + AlmSettingDto almSetting = db.almSettings().insertBitbucketCloudAlmSetting(); + db.almPats().insert(dto -> { + dto.setAlmSettingUuid(almSetting.getUuid()); + dto.setPersonalAccessToken("abc:xyz"); + dto.setUserUuid(user.getUuid()); + }); + + SearchBitbucketcloudReposWsResponse response = ws.newRequest() + .setParam("almSetting", almSetting.getKey()) + .setParam("p", "1") + .setParam("ps", "100") + .executeProtobuf(SearchBitbucketcloudReposWsResponse.class); + + assertThat(response.getIsLastPage()).isFalse(); + assertThat(response.getPaging().getPageIndex()).isEqualTo(1); + assertThat(response.getPaging().getPageSize()).isEqualTo(100); + assertThat(response.getRepositoriesList()) + .extracting(BBCRepo::getUuid, BBCRepo::getName, BBCRepo::getSlug, BBCRepo::getProjectKey, BBCRepo::getWorkspace) + .containsExactlyInAnyOrder( + tuple("REPO-UUID-ONE", "repoName1", "repo-slug-1", "projectKey1", almSetting.getAppId())); + } + + @Test + public void return_empty_list_when_no_bbs_repo() { + RepositoryList repositoryList = new RepositoryList(null, emptyList(), 1, 100); + when(bitbucketCloudRestClient.searchRepos(any(), any(), any(), any(), any())).thenReturn(repositoryList); + UserDto user = db.users().insertUser(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + AlmSettingDto almSetting = db.almSettings().insertBitbucketCloudAlmSetting(); + db.almPats().insert(dto -> { + dto.setAlmSettingUuid(almSetting.getUuid()); + dto.setPersonalAccessToken("abc:xyz"); + dto.setUserUuid(user.getUuid()); + }); + ProjectDto projectDto = db.components().insertPrivateProjectDto(); + db.almSettings().insertBitbucketCloudProjectAlmSetting(almSetting, projectDto, s -> s.setAlmRepo("projectKey2"), s -> s.setAlmSlug("repo-slug-2")); + + SearchBitbucketcloudReposWsResponse response = ws.newRequest() + .setParam("almSetting", almSetting.getKey()) + .executeProtobuf(SearchBitbucketcloudReposWsResponse.class); + + assertThat(response.getIsLastPage()).isTrue(); + assertThat(response.getRepositoriesList()).isEmpty(); + } + + @Test + public void fail_check_pat_alm_setting_not_found() { + UserDto user = db.users().insertUser(); + userSession.logIn(user).addPermission(PROVISION_PROJECTS); + AlmPatDto almPatDto = newAlmPatDto(); + db.getDbClient().almPatDao().insert(db.getSession(), almPatDto); + + TestRequest request = ws.newRequest().setParam("almSetting", "testKey"); + + assertThatThrownBy(request::execute) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("ALM Setting 'testKey' not found"); + } + + @Test + public void fail_when_not_logged_in() { + TestRequest request = ws.newRequest().setParam("almSetting", "anyvalue"); + + assertThatThrownBy(request::execute) + .isInstanceOf(UnauthorizedException.class); + } + + @Test + public void fail_when_no_creation_project_permission() { + UserDto user = db.users().insertUser(); + userSession.logIn(user); + + TestRequest request = ws.newRequest().setParam("almSetting", "anyvalue"); + + assertThatThrownBy(request::execute) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("Insufficient privileges"); + } + + @Test + public void definition() { + WebService.Action def = ws.getDef(); + + assertThat(def.since()).isEqualTo("9.0"); + assertThat(def.isPost()).isFalse(); + assertThat(def.params()) + .extracting(WebService.Param::key, WebService.Param::isRequired) + .containsExactlyInAnyOrder( + tuple("almSetting", true), + tuple("repositoryName", false), + tuple(PAGE, false), + tuple(PAGE_SIZE, false)); + } + + @NotNull + private RepositoryList getRepositoryList() { + return new RepositoryList( + "http://next.url", + asList(getBBCRepo1()), + 1, + 100); + } + + private Repository getBBCRepo1() { + Project project1 = new Project("PROJECT-UUID-ONE", "projectKey1", "projectName1"); + return new Repository("REPO-UUID-ONE", "repo-slug-1", "repoName1", project1); + } + +} diff --git a/sonar-ws/src/main/protobuf/ws-alm_integrations.proto b/sonar-ws/src/main/protobuf/ws-alm_integrations.proto index 68ad17281da..465293d5492 100644 --- a/sonar-ws/src/main/protobuf/ws-alm_integrations.proto +++ b/sonar-ws/src/main/protobuf/ws-alm_integrations.proto @@ -32,6 +32,13 @@ message SearchBitbucketserverReposWsResponse { repeated BBSRepo repositories = 2; } +// WS api/alm_integrations/search_bibucketcloud_repos +message SearchBitbucketcloudReposWsResponse { + optional sonarqube.ws.commons.Paging paging = 1; + optional bool isLastPage = 2; + repeated BBCRepo repositories = 3; +} + // WS api/alm_integrations/search_azure_repos message SearchAzureReposWsResponse { repeated AzureRepo repositories = 2; @@ -55,6 +62,15 @@ message BBSRepo { optional string projectKey = 5; } +message BBCRepo { + optional string slug = 1; + optional string uuid = 2; + optional string name = 3; + optional string sqProjectKey = 4; + optional string projectKey = 5; + optional string workspace = 6; +} + message AzureRepo { optional string name = 1; optional string projectName = 2; -- 2.39.5