From 99bf48a4107238ade798c32b9a0e17b196d16c90 Mon Sep 17 00:00:00 2001 From: Antoine Vigneau Date: Wed, 27 Mar 2024 11:55:28 +0100 Subject: [PATCH] SONAR-21922 API v2 endpoint for project-bindings --- .../alm/setting/ProjectAlmSettingDaoIT.java | 111 ++++++++- .../db/alm/setting/ProjectAlmSettingDao.java | 17 +- .../alm/setting/ProjectAlmSettingMapper.java | 8 + .../alm/setting/ProjectAlmSettingQuery.java | 26 ++ .../alm/setting/ProjectAlmSettingMapper.xml | 43 +++- .../service/ProjectBindingsSearchRequest.java | 31 +++ .../service/ProjectBindingsService.java | 73 ++++++ .../projectbindings/service/package-info.java | 23 ++ .../service/ProjectBindingsServiceTest.java | 122 ++++++++++ .../org/sonar/server/v2/WebApiEndpoints.java | 2 + .../DefaultProjectBindingsController.java | 87 +++++++ .../controller/ProjectBindingsController.java | 66 +++++ .../controller/package-info.java | 23 ++ .../projectbindings/model/ProjectBinding.java | 43 ++++ .../projectbindings/model/package-info.java | 23 ++ .../ProjectBindingsSearchRestRequest.java | 41 ++++ .../projectbindings/request/package-info.java | 23 ++ .../ProjectBindingsSearchRestResponse.java | 27 +++ .../response/package-info.java | 23 ++ .../v2/config/PlatformLevel4WebConfig.java | 8 + .../DefaultProjectBindingsControllerTest.java | 229 ++++++++++++++++++ .../platformlevel/PlatformLevel4.java | 2 + 22 files changed, 1045 insertions(+), 6 deletions(-) create mode 100644 server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingQuery.java create mode 100644 server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsSearchRequest.java create mode 100644 server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsService.java create mode 100644 server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/package-info.java create mode 100644 server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/ProjectBindingsServiceTest.java create mode 100644 server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsController.java create mode 100644 server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/ProjectBindingsController.java create mode 100644 server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/package-info.java create mode 100644 server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/model/ProjectBinding.java create mode 100644 server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/model/package-info.java create mode 100644 server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/request/ProjectBindingsSearchRestRequest.java create mode 100644 server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/request/package-info.java create mode 100644 server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/response/ProjectBindingsSearchRestResponse.java create mode 100644 server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/response/package-info.java create mode 100644 server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsControllerTest.java diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/alm/setting/ProjectAlmSettingDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/alm/setting/ProjectAlmSettingDaoIT.java index 463154e3646..8c8d9d84b95 100644 --- a/server/sonar-db-dao/src/it/java/org/sonar/db/alm/setting/ProjectAlmSettingDaoIT.java +++ b/server/sonar-db-dao/src/it/java/org/sonar/db/alm/setting/ProjectAlmSettingDaoIT.java @@ -21,9 +21,17 @@ package org.sonar.db.alm.setting; import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.IntStream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.sonar.api.impl.utils.TestSystem2; import org.sonar.core.util.UuidFactory; import org.sonar.db.DbSession; @@ -31,6 +39,11 @@ import org.sonar.db.DbTester; import org.sonar.db.audit.NoOpAuditPersister; import org.sonar.db.project.ProjectDto; +import static java.util.Arrays.stream; +import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import static org.mockito.Mockito.mock; @@ -126,12 +139,13 @@ class ProjectAlmSettingDaoIT { .containsExactlyInAnyOrder(githubProject); } - private ProjectAlmSettingDto createAlmProject(AlmSettingDto almSettingsDto) { + private ProjectAlmSettingDto createAlmProject(AlmSettingDto almSettingsDto, Consumer... populators) { ProjectDto project = db.components().insertPrivateProject().getProjectDto(); when(uuidFactory.create()).thenReturn(project.getUuid() + "_set"); - ProjectAlmSettingDto githubProjectAlmSettingDto = newGithubProjectAlmSettingDto(almSettingsDto, project); - underTest.insertOrUpdate(dbSession, githubProjectAlmSettingDto, almSettingsDto.getKey(), project.getName(), project.getKey()); - return githubProjectAlmSettingDto; + ProjectAlmSettingDto projectAlmSettingDto = newGithubProjectAlmSettingDto(almSettingsDto, project); + stream(populators).forEach(p -> p.accept(projectAlmSettingDto)); + underTest.insertOrUpdate(dbSession, projectAlmSettingDto, almSettingsDto.getKey(), project.getName(), project.getKey()); + return projectAlmSettingDto; } @Test @@ -198,6 +212,95 @@ class ProjectAlmSettingDaoIT { tuple(project2.getUuid(), almSettingsDto.getAlm().getId(), almSettingsDto.getUrl(), true)); } + @Test + void selectByUuid_whenNoResult_returnsEmptyOptional() { + Optional dto = underTest.selectByUuid(dbSession, "inexistantUuid"); + assertThat(dto).isEmpty(); + } + + @Test + void selectByUuid_whenResult_returnsIt() { + ProjectAlmSettingDto expectedDto = createAlmProject(db.almSettings().insertGitHubAlmSetting()); + + Optional actualDto = underTest.selectByUuid(dbSession, expectedDto.getUuid()); + + assertThat(actualDto) + .isPresent().get() + .usingRecursiveComparison() + .isEqualTo(expectedDto); + } + + @Test + void selectProjectAlmSettings_whenNoResult_returnsEmptyList() { + List dtos = underTest.selectProjectAlmSettings(dbSession, new ProjectAlmSettingQuery("repository", "almSettingUuid"), 1, 100); + assertThat(dtos).isEmpty(); + } + + @Test + void selectProjectAlmSettings_whenResults_returnsThem() { + AlmSettingDto matchingAlmSettingDto = db.almSettings().insertGitHubAlmSetting(); + AlmSettingDto notMatchingAlmSettingDto = db.almSettings().insertGitHubAlmSetting(); + ProjectAlmSettingDto matchingRepo = createAlmProject(matchingAlmSettingDto, dto -> dto.setAlmRepo("matchingRepo")); + ProjectAlmSettingDto notMatchingRepo = createAlmProject(matchingAlmSettingDto, dto -> dto.setAlmRepo("whatever")); + ProjectAlmSettingDto matchingAlmSetting = createAlmProject(matchingAlmSettingDto, dto -> dto.setAlmRepo("matchingRepo")); + ProjectAlmSettingDto notMatchingAlmSetting = createAlmProject(notMatchingAlmSettingDto, dto -> dto.setAlmRepo("matchingRepo")); + + List dtos = underTest.selectProjectAlmSettings(dbSession, new ProjectAlmSettingQuery("matchingRepo", matchingAlmSettingDto.getUuid()), 1, 100); + assertThat(dtos) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder(matchingRepo, matchingAlmSetting); + } + + private static Object[][] paginationTestCases() { + return new Object[][]{ + {100, 1, 5}, + {100, 3, 18}, + {2075, 41, 50}, + {0, 2, 5}, + }; + } + + @ParameterizedTest + @MethodSource("paginationTestCases") + void selectProjectAlmSettings_whenUsingPagination_findsTheRightResults(int numberToGenerate, int offset, int limit) { + when(uuidFactory.create()).thenAnswer(answer -> UUID.randomUUID().toString()); + + Map allProjectAlmSettingsDtos = generateProjectAlmSettingsDtos(numberToGenerate); + + ProjectAlmSettingQuery query = new ProjectAlmSettingQuery(null, null); + List projectAlmSettingDtos = underTest.selectProjectAlmSettings(dbSession, query, offset, limit); + + Set expectedDtos = getExpectedProjectAlmSettingDtos(offset, limit, allProjectAlmSettingsDtos); + + assertThat(projectAlmSettingDtos).usingRecursiveFieldByFieldElementComparator().containsExactlyInAnyOrderElementsOf(expectedDtos); + assertThat(underTest.countProjectAlmSettings(dbSession, query)).isEqualTo(numberToGenerate); + } + + private Map generateProjectAlmSettingsDtos(int numberToGenerate) { + if (numberToGenerate == 0) { + return emptyMap(); + } + Map result = IntStream.range(1000, 1000 + numberToGenerate) + .mapToObj(i -> underTest.insertOrUpdate(dbSession, new ProjectAlmSettingDto() + .setAlmRepo("repo_" + i) + .setAlmSettingUuid("almSettingUuid_" + i) + .setProjectUuid("projectUuid_" + i) + .setMonorepo(false), + "key_" + i, "projectName_" + i, "projectKey_" + i)) + .collect(toMap(ProjectAlmSettingDto::getAlmRepo, Function.identity())); + db.commit(); + return result; + } + + private Set getExpectedProjectAlmSettingDtos(int offset, int limit, Map allProjectAlmSettingsDtos) { + if (allProjectAlmSettingsDtos.isEmpty()) { + return emptySet(); + } + return IntStream.range(1000 + (offset - 1) * limit, 1000 + offset * limit) + .mapToObj(i -> allProjectAlmSettingsDtos.get("repo_" + i)) + .collect(toSet()); + } + @Test void update_existing_binding() { when(uuidFactory.create()).thenReturn(A_UUID); diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingDao.java index c67f5d292b1..da44563273d 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingDao.java @@ -28,6 +28,7 @@ import org.sonar.api.utils.System2; import org.sonar.core.util.UuidFactory; import org.sonar.db.Dao; import org.sonar.db.DbSession; +import org.sonar.db.Pagination; import org.sonar.db.audit.AuditPersister; import org.sonar.db.audit.model.DevOpsPlatformSettingNewValue; import org.sonar.db.project.ProjectDto; @@ -46,7 +47,7 @@ public class ProjectAlmSettingDao implements Dao { this.auditPersister = auditPersister; } - public void insertOrUpdate(DbSession dbSession, ProjectAlmSettingDto projectAlmSettingDto, String key, String projectName, String projectKey) { + public ProjectAlmSettingDto insertOrUpdate(DbSession dbSession, ProjectAlmSettingDto projectAlmSettingDto, String key, String projectName, String projectKey) { String uuid = uuidFactory.create(); long now = system2.now(); ProjectAlmSettingMapper mapper = getMapper(dbSession); @@ -66,6 +67,8 @@ public class ProjectAlmSettingDao implements Dao { } else { auditPersister.addDevOpsPlatformSetting(dbSession, value); } + + return projectAlmSettingDto; } public void deleteByProject(DbSession dbSession, ProjectDto project) { @@ -84,6 +87,18 @@ public class ProjectAlmSettingDao implements Dao { return getMapper(dbSession).countByAlmSettingUuid(almSetting.getUuid()); } + public int countProjectAlmSettings(DbSession dbSession, ProjectAlmSettingQuery query) { + return getMapper(dbSession).countByQuery(query); + } + + public List selectProjectAlmSettings(DbSession dbSession, ProjectAlmSettingQuery query, int page, int pageSize) { + return getMapper(dbSession).selectByQuery(query, Pagination.forPage(page).andSize(pageSize)); + } + + public Optional selectByUuid(DbSession dbSession, String uuid) { + return Optional.ofNullable(getMapper(dbSession).selectByUuid(uuid)); + } + public Optional selectByProject(DbSession dbSession, ProjectDto project) { return selectByProject(dbSession, project.getUuid()); } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingMapper.java index 2d7c999ae5b..40faf7c10cf 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingMapper.java @@ -23,14 +23,22 @@ import java.util.List; import java.util.Set; import javax.annotation.CheckForNull; import org.apache.ibatis.annotations.Param; +import org.sonar.db.Pagination; public interface ProjectAlmSettingMapper { + @CheckForNull + ProjectAlmSettingDto selectByUuid(@Param("uuid") String uuid); + @CheckForNull ProjectAlmSettingDto selectByProjectUuid(@Param("projectUuid") String projectUuid); int countByAlmSettingUuid(@Param("almSettingUuid") String almSettingUuid); + int countByQuery(@Param("query") ProjectAlmSettingQuery query); + + List selectByQuery(@Param("query") ProjectAlmSettingQuery query, @Param("pagination") Pagination pagination); + void insert(@Param("dto") ProjectAlmSettingDto projectAlmSettingDto, @Param("uuid") String uuid, @Param("now") long now); int update(@Param("dto") ProjectAlmSettingDto projectAlmSettingDto, @Param("now") long now); diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingQuery.java b/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingQuery.java new file mode 100644 index 00000000000..eae9ad5e01c --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/alm/setting/ProjectAlmSettingQuery.java @@ -0,0 +1,26 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.db.alm.setting; + +import javax.annotation.Nullable; + +public record ProjectAlmSettingQuery(@Nullable String repository, @Nullable String almSettingUuid +) { +} diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/alm/setting/ProjectAlmSettingMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/alm/setting/ProjectAlmSettingMapper.xml index 3a07307ee05..c212b88a1ea 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/alm/setting/ProjectAlmSettingMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/alm/setting/ProjectAlmSettingMapper.xml @@ -27,6 +27,14 @@ alm_settings alm_settings on pas.alm_setting_uuid = alm_settings.uuid + + + + + + + + + + 1=1 + + AND ( + (lower(p.alm_repo) = lower(#{query.repository, jdbcType=VARCHAR})) + OR (lower(p.alm_slug) = lower(#{query.repository, jdbcType=VARCHAR})) + ) + + + AND p.alm_setting_uuid = #{query.almSettingUuid, jdbcType=VARCHAR} + + + + DELETE FROM project_alm_settings WHERE alm_setting_uuid = #{almSettingUuid, jdbcType=VARCHAR} diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsSearchRequest.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsSearchRequest.java new file mode 100644 index 00000000000..4355612a415 --- /dev/null +++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsSearchRequest.java @@ -0,0 +1,31 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.common.projectbindings.service; + +import javax.annotation.Nullable; + +public record ProjectBindingsSearchRequest( + @Nullable String repository, + @Nullable String dopSettingId, + Integer page, + Integer pageSize +) { + +} diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsService.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsService.java new file mode 100644 index 00000000000..6dd548d0347 --- /dev/null +++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/ProjectBindingsService.java @@ -0,0 +1,73 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.common.projectbindings.service; + +import java.util.List; +import java.util.Optional; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingQuery; +import org.sonar.db.project.ProjectDto; +import org.sonar.server.common.SearchResults; + +public class ProjectBindingsService { + + private final DbClient dbClient; + + public ProjectBindingsService(DbClient dbClient) { + this.dbClient = dbClient; + } + + public Optional findProjectBindingByUuid(String uuid) { + try (DbSession session = dbClient.openSession(false)) { + return dbClient.projectAlmSettingDao().selectByUuid(session, uuid); + } + } + + public SearchResults findProjectBindingsByRequest(ProjectBindingsSearchRequest request) { + ProjectAlmSettingQuery query = buildProjectAlmSettingQuery(request); + try (DbSession session = dbClient.openSession(false)) { + int total = dbClient.projectAlmSettingDao().countProjectAlmSettings(session, query); + if (request.pageSize() == 0) { + return new SearchResults<>(List.of(), total); + } + List searchResults = performSearch(session, query, request.page(), request.pageSize()); + return new SearchResults<>(searchResults, total); + } + } + + private static ProjectAlmSettingQuery buildProjectAlmSettingQuery(ProjectBindingsSearchRequest request) { + return new ProjectAlmSettingQuery(request.repository(), request.dopSettingId()); + } + + private List performSearch(DbSession dbSession, ProjectAlmSettingQuery query, int page, int pageSize) { + return dbClient.projectAlmSettingDao().selectProjectAlmSettings(dbSession, query, page, pageSize) + .stream() + .toList(); + } + + public Optional findProjectFromBinding(ProjectAlmSettingDto projectAlmSettingDto) { + try (DbSession session = dbClient.openSession(false)) { + return dbClient.projectDao().selectByUuid(session, projectAlmSettingDto.getProjectUuid()); + } + } + +} diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/package-info.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/package-info.java new file mode 100644 index 00000000000..2cd3f3c700f --- /dev/null +++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/projectbindings/service/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.common.projectbindings.service; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/ProjectBindingsServiceTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/ProjectBindingsServiceTest.java new file mode 100644 index 00000000000..46f3790d707 --- /dev/null +++ b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/projectbindings/service/ProjectBindingsServiceTest.java @@ -0,0 +1,122 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.common.projectbindings.service; + + +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.alm.setting.ProjectAlmSettingDao; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; +import org.sonar.db.alm.setting.ProjectAlmSettingQuery; +import org.sonar.server.common.SearchResults; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ProjectBindingsServiceTest { + + private static final String UUID = "uuid"; + public static final String REPO_QUERY = "repoQuery"; + public static final String ALM_SETTING_UUID_QUERY = "almSettingUuidQuery"; + + @Mock + private DbSession dbSession; + @Mock + private DbClient dbClient; + + @InjectMocks + private ProjectBindingsService underTest; + + @Captor + private ArgumentCaptor daoQueryCaptor; + + @BeforeEach + void setup() { + when(dbClient.openSession(false)).thenReturn(dbSession); + when(dbClient.projectAlmSettingDao()).thenReturn(mock(ProjectAlmSettingDao.class)); + } + + @Test + void findProjectBindingByUuid_whenNoResult_returnsOptionalEmpty() { + when(dbClient.projectAlmSettingDao().selectByUuid(dbSession, UUID)).thenReturn(Optional.empty()); + + assertThat(underTest.findProjectBindingByUuid(UUID)).isEmpty(); + } + + @Test + void findProjectBindingByUuid_whenResult_returnsIt() { + ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); + when(dbClient.projectAlmSettingDao().selectByUuid(dbSession, UUID)).thenReturn(Optional.of(projectAlmSettingDto)); + + assertThat(underTest.findProjectBindingByUuid(UUID)).contains(projectAlmSettingDto); + } + + @Test + void findProjectBindingsByRequest_whenResults_returnsThem() { + ProjectAlmSettingDto dto1 = mock(); + ProjectAlmSettingDto dto2 = mock(); + List expectedResults = List.of(dto1, dto2); + + when(dbClient.projectAlmSettingDao().selectProjectAlmSettings(eq(dbSession), daoQueryCaptor.capture(), eq(12), eq(42))) + .thenReturn(expectedResults); + when(dbClient.projectAlmSettingDao().countProjectAlmSettings(eq(dbSession), any())) + .thenReturn(expectedResults.size()); + + ProjectBindingsSearchRequest request = new ProjectBindingsSearchRequest(REPO_QUERY, ALM_SETTING_UUID_QUERY, 12, 42); + SearchResults actualResults = underTest.findProjectBindingsByRequest(request); + + assertThat(daoQueryCaptor.getValue().repository()).isEqualTo(REPO_QUERY); + assertThat(daoQueryCaptor.getValue().almSettingUuid()).isEqualTo(ALM_SETTING_UUID_QUERY); + assertThat(actualResults.total()).isEqualTo(expectedResults.size()); + assertThat(actualResults.searchResults()).containsExactlyInAnyOrderElementsOf(expectedResults); + } + + @Test + void findProjectBindingsByRequest_whenPageSize0_returnsOnlyTotal() { + when(dbClient.projectAlmSettingDao().countProjectAlmSettings(eq(dbSession), any())) + .thenReturn(12); + + ProjectBindingsSearchRequest request = new ProjectBindingsSearchRequest(null, null, 42, 0); + SearchResults actualResults = underTest.findProjectBindingsByRequest(request); + + assertThat(actualResults.total()).isEqualTo(12); + assertThat(actualResults.searchResults()).isEmpty(); + + verify(dbClient.projectAlmSettingDao(), never()).selectProjectAlmSettings(eq(dbSession), any(), anyInt(), anyInt()); + } + +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java index c906493f7e1..4a03ac6e898 100644 --- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java @@ -40,6 +40,8 @@ public class WebApiEndpoints { public static final String BOUND_PROJECTS_ENDPOINT = DOP_TRANSLATION_DOMAIN + "/bound-projects"; + public static final String PROJECT_BINDINGS_ENDPOINT = DOP_TRANSLATION_DOMAIN + "/project-bindings"; + public static final String DOP_SETTINGS_ENDPOINT = DOP_TRANSLATION_DOMAIN + "/dop-settings"; public static final String INTERNAL = "internal"; diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsController.java new file mode 100644 index 00000000000..58c84b76cea --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsController.java @@ -0,0 +1,87 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.v2.api.projectbindings.controller; + +import java.util.List; +import java.util.Optional; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; +import org.sonar.db.project.ProjectDto; +import org.sonar.server.common.SearchResults; +import org.sonar.server.common.projectbindings.service.ProjectBindingsSearchRequest; +import org.sonar.server.common.projectbindings.service.ProjectBindingsService; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.user.UserSession; +import org.sonar.server.v2.api.model.RestPage; +import org.sonar.server.v2.api.projectbindings.model.ProjectBinding; +import org.sonar.server.v2.api.projectbindings.request.ProjectBindingsSearchRestRequest; +import org.sonar.server.v2.api.projectbindings.response.ProjectBindingsSearchRestResponse; +import org.sonar.server.v2.api.response.PageRestResponse; + +import static org.sonar.api.web.UserRole.USER; +import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; + +public class DefaultProjectBindingsController implements ProjectBindingsController { + + private final UserSession userSession; + private final ProjectBindingsService projectBindingsService; + + public DefaultProjectBindingsController(UserSession userSession, ProjectBindingsService projectBindingsService) { + this.userSession = userSession; + this.projectBindingsService = projectBindingsService; + } + + @Override + public ProjectBinding getProjectBinding(String id) { + Optional projectAlmSettingDto = projectBindingsService.findProjectBindingByUuid(id); + if (projectAlmSettingDto.isPresent()) { + ProjectDto projectDto = projectBindingsService.findProjectFromBinding(projectAlmSettingDto.get()) + .orElseThrow(() -> new IllegalStateException(String.format("Project (uuid '%s') not found for binding '%s'", projectAlmSettingDto.get().getProjectUuid(), id))); + userSession.checkEntityPermission(USER, projectDto); + return toProjectBinding(projectAlmSettingDto.get()); + } else { + throw new NotFoundException(String.format("Project binding '%s' not found", id)); + } + } + + @Override + public ProjectBindingsSearchRestResponse searchProjectBindings(ProjectBindingsSearchRestRequest restRequest, RestPage restPage) { + userSession.checkLoggedIn().checkPermission(PROVISION_PROJECTS); + ProjectBindingsSearchRequest serviceRequest = new ProjectBindingsSearchRequest(restRequest.repository(), restRequest.dopSettingId(), restPage.pageIndex(), restPage.pageSize()); + SearchResults searchResults = projectBindingsService.findProjectBindingsByRequest(serviceRequest); + List projectBindings = toProjectBindings(searchResults); + return new ProjectBindingsSearchRestResponse(projectBindings, new PageRestResponse(restPage.pageIndex(), restPage.pageSize(), searchResults.total())); + } + + private static List toProjectBindings(SearchResults searchResults) { + return searchResults.searchResults().stream() + .map(DefaultProjectBindingsController::toProjectBinding) + .toList(); + } + + private static ProjectBinding toProjectBinding(ProjectAlmSettingDto dto) { + return new ProjectBinding( + dto.getUuid(), + dto.getAlmSettingUuid(), + dto.getProjectUuid(), + dto.getAlmRepo(), + dto.getAlmSlug() + ); + } +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/ProjectBindingsController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/ProjectBindingsController.java new file mode 100644 index 00000000000..baf77a69ddf --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/ProjectBindingsController.java @@ -0,0 +1,66 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.v2.api.projectbindings.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.extensions.Extension; +import io.swagger.v3.oas.annotations.extensions.ExtensionProperty; +import javax.validation.Valid; +import org.sonar.server.v2.api.model.RestPage; +import org.sonar.server.v2.api.projectbindings.model.ProjectBinding; +import org.sonar.server.v2.api.projectbindings.request.ProjectBindingsSearchRestRequest; +import org.sonar.server.v2.api.projectbindings.response.ProjectBindingsSearchRestResponse; +import org.springdoc.api.annotations.ParameterObject; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.sonar.server.v2.WebApiEndpoints.INTERNAL; +import static org.sonar.server.v2.WebApiEndpoints.PROJECT_BINDINGS_ENDPOINT; + +@RequestMapping(PROJECT_BINDINGS_ENDPOINT) +@RestController +public interface ProjectBindingsController { + + @GetMapping(path = "/{id}") + @Operation( + operationId = "getProjectBinding", + summary = "Fetch a single Project Binding", + extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")}) + ) + ProjectBinding getProjectBinding( + @PathVariable("id") @Parameter(description = "The id of the project-bindings to fetch.", required = true, in = ParameterIn.PATH) String id + ); + + @GetMapping() + @Operation( + operationId = "getProjectBindingByProjectId", + summary = "Search across project bindings", + extensions = @Extension(properties = {@ExtensionProperty(name = INTERNAL, value = "true")}) + ) + ProjectBindingsSearchRestResponse searchProjectBindings ( + @Valid @ParameterObject ProjectBindingsSearchRestRequest projectBindingsSearchRestRequest, + @Valid @ParameterObject RestPage restPage + ); + +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/package-info.java new file mode 100644 index 00000000000..f61b5ad344b --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/controller/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.v2.api.projectbindings.controller; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/model/ProjectBinding.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/model/ProjectBinding.java new file mode 100644 index 00000000000..33cbde2320a --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/model/ProjectBinding.java @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.v2.api.projectbindings.model; + +import javax.annotation.Nullable; +import javax.validation.constraints.NotNull; + +public record ProjectBinding ( + + @NotNull + String id, + + @NotNull + String dopSettings, + + @NotNull + String projectId, + + @Nullable + String repository, + + @Nullable + String slug + +) { +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/model/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/model/package-info.java new file mode 100644 index 00000000000..047dea91395 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/model/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.v2.api.projectbindings.model; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/request/ProjectBindingsSearchRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/request/ProjectBindingsSearchRestRequest.java new file mode 100644 index 00000000000..345430983a3 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/request/ProjectBindingsSearchRestRequest.java @@ -0,0 +1,41 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.v2.api.projectbindings.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.annotation.Nullable; + +public record ProjectBindingsSearchRestRequest ( + + @Nullable + @Schema( + description = """ + Filter on the repository name. + This parameter performs an exact, case insensitive, match. + """) + String repository, + + @Nullable + @Schema(description = "Filter on the DevOps Platform setting id.") + String dopSettingId + +) { + +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/request/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/request/package-info.java new file mode 100644 index 00000000000..f99874be8f6 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/request/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.v2.api.projectbindings.request; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/response/ProjectBindingsSearchRestResponse.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/response/ProjectBindingsSearchRestResponse.java new file mode 100644 index 00000000000..306ef944ab0 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/response/ProjectBindingsSearchRestResponse.java @@ -0,0 +1,27 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.v2.api.projectbindings.response; + +import java.util.List; +import org.sonar.server.v2.api.projectbindings.model.ProjectBinding; +import org.sonar.server.v2.api.response.PageRestResponse; + +public record ProjectBindingsSearchRestResponse(List projectBindings, PageRestResponse page) { +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/response/package-info.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/response/package-info.java new file mode 100644 index 00000000000..92a90fbd7df --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projectbindings/response/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.v2.api.projectbindings.response; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java index c48cf412bc6..01d0c91ac34 100644 --- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java @@ -33,6 +33,7 @@ import org.sonar.server.common.management.ManagedInstanceChecker; import org.sonar.server.common.platform.LivenessChecker; import org.sonar.server.common.platform.LivenessCheckerImpl; import org.sonar.server.common.project.ImportProjectService; +import org.sonar.server.common.projectbindings.service.ProjectBindingsService; import org.sonar.server.common.rule.service.RuleService; import org.sonar.server.common.text.MacroInterpreter; import org.sonar.server.common.user.service.UserService; @@ -49,6 +50,8 @@ import org.sonar.server.v2.api.group.controller.DefaultGroupController; import org.sonar.server.v2.api.group.controller.GroupController; import org.sonar.server.v2.api.membership.controller.DefaultGroupMembershipController; import org.sonar.server.v2.api.membership.controller.GroupMembershipController; +import org.sonar.server.v2.api.projectbindings.controller.DefaultProjectBindingsController; +import org.sonar.server.v2.api.projectbindings.controller.ProjectBindingsController; import org.sonar.server.v2.api.projects.controller.BoundProjectsController; import org.sonar.server.v2.api.projects.controller.DefaultBoundProjectsController; import org.sonar.server.v2.api.rule.controller.DefaultRuleController; @@ -143,4 +146,9 @@ public class PlatformLevel4WebConfig { return new DefaultDopSettingsController(userSession, dbClient); } + @Bean + public ProjectBindingsController projectBindingsController(UserSession userSession, ProjectBindingsService projectBindingsService) { + return new DefaultProjectBindingsController(userSession, projectBindingsService); + } + } diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsControllerTest.java new file mode 100644 index 00000000000..7912ad9228f --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/projectbindings/controller/DefaultProjectBindingsControllerTest.java @@ -0,0 +1,229 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.v2.api.projectbindings.controller; + +import java.util.List; +import java.util.Optional; +import org.junit.Rule; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.sonar.db.alm.setting.ProjectAlmSettingDto; +import org.sonar.db.project.ProjectDto; +import org.sonar.server.common.SearchResults; +import org.sonar.server.common.projectbindings.service.ProjectBindingsSearchRequest; +import org.sonar.server.common.projectbindings.service.ProjectBindingsService; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.v2.api.ControllerTester; +import org.springframework.test.web.servlet.MockMvc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.sonar.api.web.UserRole.ADMIN; +import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; +import static org.sonar.server.v2.WebApiEndpoints.PROJECT_BINDINGS_ENDPOINT; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class DefaultProjectBindingsControllerTest { + + public static final String UUID = "uuid"; + private static final String PROJECT_UUID = "projectUuid"; + + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + private final ProjectBindingsService projectBindingsService = mock(); + + private final MockMvc mockMvc = ControllerTester.getMockMvc(new DefaultProjectBindingsController(userSession, projectBindingsService)); + + @Test + void getProjectBinding_whenNoProjectBinding_returnsNotFound() throws Exception { + userSession.logIn(); + when(projectBindingsService.findProjectBindingByUuid(UUID)).thenReturn(Optional.empty()); + + mockMvc + .perform(get(PROJECT_BINDINGS_ENDPOINT + "/uuid")) + .andExpectAll( + status().isNotFound(), + content().json(""" + { + "message": "Project binding 'uuid' not found" + } + """)); + } + + @Test + void getProjectBinding_whenNoProject_returnsServerError() throws Exception { + userSession.logIn(); + ProjectAlmSettingDto projectAlmSettingDto = mock(); + when(projectAlmSettingDto.getProjectUuid()).thenReturn(PROJECT_UUID); + when(projectBindingsService.findProjectBindingByUuid(UUID)).thenReturn(Optional.of(projectAlmSettingDto)); + when(projectBindingsService.findProjectFromBinding(projectAlmSettingDto)).thenReturn(Optional.empty()); + + mockMvc + .perform(get(PROJECT_BINDINGS_ENDPOINT + "/uuid")) + .andExpectAll( + status().isInternalServerError(), + content().json(""" + { + "message": "Project (uuid 'projectUuid') not found for binding 'uuid'" + } + """)); + } + + @Test + void getProjectBinding_whenUserDoesntHaveProjectAdminPermissions_returnsForbidden() throws Exception { + userSession.logIn(); + ProjectAlmSettingDto projectAlmSettingDto = mock(); + when(projectBindingsService.findProjectBindingByUuid(UUID)).thenReturn(Optional.of(projectAlmSettingDto)); + when(projectBindingsService.findProjectFromBinding(projectAlmSettingDto)).thenReturn(Optional.ofNullable(mock(ProjectDto.class))); + + mockMvc + .perform(get(PROJECT_BINDINGS_ENDPOINT + "/uuid")) + .andExpectAll( + status().isForbidden(), + content().json(""" + { + "message": "Insufficient privileges" + } + """)); + } + + @Test + void getProjectBinding_whenProjectBindingAndPermissions_returnsIt() throws Exception { + ProjectAlmSettingDto projectAlmSettingDto = mockProjectAlmSettingDto("1"); + ProjectDto projectDto = mock(); + userSession.logIn().addProjectPermission(ADMIN, projectDto); + when(projectBindingsService.findProjectBindingByUuid(UUID)).thenReturn(Optional.of(projectAlmSettingDto)); + when(projectBindingsService.findProjectFromBinding(projectAlmSettingDto)).thenReturn(Optional.ofNullable(projectDto)); + + mockMvc + .perform(get(PROJECT_BINDINGS_ENDPOINT + "/uuid")) + .andExpectAll( + status().isOk(), + content().json(""" + { + "id": "uuid_1", + "dopSettings": "almSettingUuid_1", + "projectId": "projectUuid_1", + "repository": "almRepo_1", + "slug": "almSlug_1" + } + """)); + } + + @Test + void searchProjectBindings_whenUserDoesntHaveProjectProvisionPermission_returnsForbidden() throws Exception { + userSession.logIn(); + + mockMvc + .perform(get(PROJECT_BINDINGS_ENDPOINT) + .param("repository", "repo") + .param("dopSettingId", "id")) + .andExpectAll( + status().isForbidden(), + content().json(""" + { + "message": "Insufficient privileges" + } + """)); + + } + + @Test + void searchProjectBindings_whenParametersUsed_shouldForwardWithParameters() throws Exception { + userSession.logIn().addPermission(PROVISION_PROJECTS); + when(projectBindingsService.findProjectBindingsByRequest(any())).thenReturn(new SearchResults<>(List.of(), 0)); + + mockMvc + .perform(get(PROJECT_BINDINGS_ENDPOINT) + .param("repository", "repo") + .param("dopSettingId", "id") + .param("pageIndex", "12") + .param("pageSize", "42")) + .andExpect(status().isOk()); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(ProjectBindingsSearchRequest.class); + verify(projectBindingsService).findProjectBindingsByRequest(requestCaptor.capture()); + assertThat(requestCaptor.getValue().repository()).isEqualTo("repo"); + assertThat(requestCaptor.getValue().dopSettingId()).isEqualTo("id"); + assertThat(requestCaptor.getValue().page()).isEqualTo(12); + assertThat(requestCaptor.getValue().pageSize()).isEqualTo(42); + } + + @Test + void searchProjectBindings_whenResultsFound_shouldReturnsThem() throws Exception { + userSession.logIn().addPermission(PROVISION_PROJECTS); + + ProjectAlmSettingDto dto1 = mockProjectAlmSettingDto("1"); + ProjectAlmSettingDto dto2 = mockProjectAlmSettingDto("2"); + + List expectedResults = List.of(dto1, dto2); + when(projectBindingsService.findProjectBindingsByRequest(any())).thenReturn(new SearchResults<>(expectedResults, expectedResults.size())); + + mockMvc + .perform(get(PROJECT_BINDINGS_ENDPOINT) + .param("repository", "whatever") + .param("dopSettingId", "doesntmatter") + .param("pageIndex", "1") + .param("pageSize", "100")) + .andExpectAll( + status().isOk(), + content().json(""" + { + "projectBindings": [ + { + "id": "uuid_1", + "dopSettings": "almSettingUuid_1", + "projectId": "projectUuid_1", + "repository": "almRepo_1", + "slug": "almSlug_1" + }, + { + "id": "uuid_2", + "dopSettings": "almSettingUuid_2", + "projectId": "projectUuid_2", + "repository": "almRepo_2", + "slug": "almSlug_2" + } + ], + "page": { + "pageIndex": 1, + "pageSize": 100, + "total": 2 + } + } + """)); + } + + private ProjectAlmSettingDto mockProjectAlmSettingDto(String i) { + ProjectAlmSettingDto dto = mock(); + when(dto.getUuid()).thenReturn("uuid_" + i); + when(dto.getAlmSettingUuid()).thenReturn("almSettingUuid_" + i); + when(dto.getProjectUuid()).thenReturn("projectUuid_" + i); + when(dto.getAlmRepo()).thenReturn("almRepo_" + i); + when(dto.getAlmSlug()).thenReturn("almSlug_" + i); + return dto; + } + +} \ No newline at end of file diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index b8b7e924ba9..bd96d1c36d2 100644 --- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -96,6 +96,7 @@ import org.sonar.server.common.permission.GroupPermissionChanger; import org.sonar.server.common.permission.PermissionTemplateService; import org.sonar.server.common.permission.PermissionUpdater; import org.sonar.server.common.permission.UserPermissionChanger; +import org.sonar.server.common.projectbindings.service.ProjectBindingsService; import org.sonar.server.common.rule.RuleCreator; import org.sonar.server.common.rule.service.RuleService; import org.sonar.server.common.text.MacroInterpreter; @@ -592,6 +593,7 @@ public class PlatformLevel4 extends PlatformLevel { // ALM settings new AlmSettingsWsModule(), + ProjectBindingsService.class, // Project export new ProjectExportWsModule(), -- 2.39.5