From 32d9d255b2704696c3ab4aabcd2e443563c1f945 Mon Sep 17 00:00:00 2001 From: Nolwenn Cadic <98824442+Nolwenn-cadic-sonarsource@users.noreply.github.com> Date: Thu, 8 Jun 2023 16:20:36 +0200 Subject: [PATCH] SONAR-19453 New code definition is made part of project creation (#8470) Co-authored-by: Zipeng WU --- .../newcodeperiod/NewCodePeriodUtilsTest.java | 217 ++++++++++++++++ .../server/project/ws/CreateActionIT.java | 233 +++++++++++++++++- .../newcodeperiod/NewCodePeriodUtils.java | 190 ++++++++++++++ .../server/newcodeperiod/ws/SetAction.java | 113 +-------- .../sonar/server/project/ws/CreateAction.java | 128 ++++++++-- .../client/project/ProjectsWsParameters.java | 3 + 6 files changed, 755 insertions(+), 129 deletions(-) create mode 100644 server/sonar-webserver-webapi/src/it/java/org/sonar/server/newcodeperiod/NewCodePeriodUtilsTest.java create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/newcodeperiod/NewCodePeriodUtils.java diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/newcodeperiod/NewCodePeriodUtilsTest.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/newcodeperiod/NewCodePeriodUtilsTest.java new file mode 100644 index 00000000000..088b98769b8 --- /dev/null +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/newcodeperiod/NewCodePeriodUtilsTest.java @@ -0,0 +1,217 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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.newcodeperiod; + +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.utils.System2; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.component.BranchDto; +import org.sonar.db.component.ComponentDbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.SnapshotDto; +import org.sonar.db.project.ProjectDto; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.sonar.db.newcodeperiod.NewCodePeriodType.NUMBER_OF_DAYS; +import static org.sonar.db.newcodeperiod.NewCodePeriodType.PREVIOUS_VERSION; +import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH; +import static org.sonar.db.newcodeperiod.NewCodePeriodType.SPECIFIC_ANALYSIS; +import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.getNewCodeDefinitionValue; +import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.getNewCodeDefinitionValueProjectCreation; +import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.validateType; + +public class NewCodePeriodUtilsTest { + + @Rule + public DbTester db = DbTester.create(System2.INSTANCE, true); + + private static final String MAIN_BRANCH = "main"; + private ComponentDbTester componentDb = new ComponentDbTester(db); + + private DbSession dbSession = db.getSession(); + private DbClient dbClient = db.getDbClient(); + @Test + public void validateType_throw_IAE_if_no_valid_type() { + assertThatThrownBy(() -> validateType("nonValid", false, false)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid type: nonValid"); + } + + @Test + public void validateType_throw_IAE_if_type_is_invalid_for_global() { + assertThatThrownBy(() -> validateType("SPECIFIC_ANALYSIS", true, false)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid type 'SPECIFIC_ANALYSIS'. Overall setting can only be set with types: [PREVIOUS_VERSION, NUMBER_OF_DAYS]"); + } + + @Test + public void validateType_throw_IAE_if_type_is_invalid_for_project() { + assertThatThrownBy(() -> validateType("SPECIFIC_ANALYSIS", false, false)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid type 'SPECIFIC_ANALYSIS'. Projects can only be set with types: [PREVIOUS_VERSION, NUMBER_OF_DAYS, REFERENCE_BRANCH]"); + } + + @Test + public void validateType_return_type_for_branch() { + assertThat(validateType("REFERENCE_BRANCH", false, true)).isEqualTo(REFERENCE_BRANCH); + } + + @Test + public void validateType_return_type_for_project() { + assertThat(validateType("REFERENCE_BRANCH", false, false)).isEqualTo(REFERENCE_BRANCH); + } + + @Test + public void validateType_return_type_for_overall() { + assertThat(validateType("PREVIOUS_VERSION", true, false)).isEqualTo(PREVIOUS_VERSION); + } + + @Test + public void getNCDValue_throw_IAE_if_no_value_for_days() { + assertThatThrownBy(() -> getNewCodeDefinitionValue(dbSession, dbClient, NUMBER_OF_DAYS, null, null, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("New code definition type 'NUMBER_OF_DAYS' requires a value"); + } + + @Test + public void getNCDValue_throw_IAE_if_no_value_for_reference_branch() { + assertThatThrownBy(() -> getNewCodeDefinitionValue(dbSession, dbClient, REFERENCE_BRANCH, null, null, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("New code definition type 'REFERENCE_BRANCH' requires a value"); + } + + @Test + public void getNCDValue_throw_IAE_if_no_value_for_analysis() { + assertThatThrownBy(() -> getNewCodeDefinitionValue(dbSession, dbClient, SPECIFIC_ANALYSIS, null, null, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("New code definition type 'SPECIFIC_ANALYSIS' requires a value"); + } + + @Test + public void getNCDValue_throw_IAE_if_days_is_invalid() { + assertThatThrownBy(() -> getNewCodeDefinitionValue(dbSession, dbClient, NUMBER_OF_DAYS, null, null, "unknown")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Failed to parse number of days: unknown"); + } + + @Test + public void getNCDValue_throw_IAE_if_previous_version_type_and_value_provided() { + assertThatThrownBy(() -> getNewCodeDefinitionValue(dbSession, dbClient, PREVIOUS_VERSION, null, null, "someValue")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unexpected value for type 'PREVIOUS_VERSION'"); + } + + @Test + public void getNCDValue_return_empty_for_previous_version_type() { + assertThat(getNewCodeDefinitionValue(dbSession, dbClient, PREVIOUS_VERSION, null, null, null)).isEmpty(); + } + + @Test + public void getNCDValue_return_days_value_for_number_of_days_type() { + String numberOfDays = "30"; + + assertThat(getNewCodeDefinitionValue(dbSession, dbClient, NUMBER_OF_DAYS, null, null, numberOfDays)) + .isPresent() + .get() + .isEqualTo(numberOfDays); + } + + @Test + public void getNCDValue_return_specific_analysis_uuid_for_specific_analysis_type() { + ComponentDto project = componentDb.insertPublicProject().getMainBranchComponent(); + SnapshotDto analysisMaster = db.components().insertSnapshot(project); + ProjectDto projectDto = new ProjectDto().setUuid(project.uuid()); + BranchDto branchDto = new BranchDto().setUuid(project.uuid()); + String numberOfDays = "30"; + + assertThat(getNewCodeDefinitionValue(dbSession, dbClient, SPECIFIC_ANALYSIS, projectDto, branchDto, analysisMaster.getUuid())) + .isPresent() + .get() + .isEqualTo(analysisMaster.getUuid()); + } + + @Test + public void getNCDValue_return_branch_value_for_reference_branch_type() { + String branchKey = "main"; + + assertThat(getNewCodeDefinitionValue(dbSession, dbClient, REFERENCE_BRANCH, null, null, branchKey)) + .isPresent() + .get() + .isEqualTo(branchKey); + } + + @Test + public void getNCDValueProjectCreation_throw_IAE_if_no_value_for_days() { + assertThatThrownBy(() -> getNewCodeDefinitionValueProjectCreation(NUMBER_OF_DAYS, null, MAIN_BRANCH)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("New code definition type 'NUMBER_OF_DAYS' requires a value"); + } + + @Test + public void getNCDValueProjectCreation_throw_IAE_if_days_is_invalid() { + assertThatThrownBy(() -> getNewCodeDefinitionValueProjectCreation(NUMBER_OF_DAYS, "unknown", MAIN_BRANCH)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Failed to parse number of days: unknown"); + } + + @Test + public void getNCDValueProjectCreation_throw_IAE_if_previous_version_type_and_value_provided() { + assertThatThrownBy(() -> getNewCodeDefinitionValueProjectCreation(PREVIOUS_VERSION, "someValue", MAIN_BRANCH)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unexpected value for type 'PREVIOUS_VERSION'"); + } + + @Test + public void getNCDValueProjectCreation_return_empty_for_previous_version_type() { + assertThat(getNewCodeDefinitionValueProjectCreation(PREVIOUS_VERSION, null, MAIN_BRANCH)).isEmpty(); + } + + @Test + public void getNCDValueProjectCreation_return_days_value_for_number_of_days_type() { + String numberOfDays = "30"; + + assertThat(getNewCodeDefinitionValueProjectCreation(NUMBER_OF_DAYS, numberOfDays, MAIN_BRANCH)) + .isPresent() + .get() + .isEqualTo(numberOfDays); + } + + @Test + public void getNCDValueProjectCreation_return_branch_value_for_reference_branch_type() { + String branchKey = "main"; + + assertThat(getNewCodeDefinitionValueProjectCreation(REFERENCE_BRANCH, null, branchKey)) + .isPresent() + .get() + .isEqualTo(branchKey); + } + + @Test + public void getNCDValueProjectCreation_throw_IAE_fiif_reference_branch_type_and_value_provided() { + assertThatThrownBy(() -> getNewCodeDefinitionValueProjectCreation(REFERENCE_BRANCH, "someValue", MAIN_BRANCH)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unexpected value for type 'REFERENCE_BRANCH'"); + } + +} diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/project/ws/CreateActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/project/ws/CreateActionIT.java index cfc0f3d3d1d..dcfa88bdd86 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/project/ws/CreateActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/project/ws/CreateActionIT.java @@ -20,17 +20,21 @@ package org.sonar.server.project.ws; import com.google.common.base.Strings; +import java.util.Optional; import javax.annotation.Nullable; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.sonar.api.server.ws.WebService; import org.sonar.api.utils.System2; +import org.sonar.core.platform.EditionProvider; +import org.sonar.core.platform.PlatformEditionProvider; import org.sonar.core.util.SequenceUuidFactory; import org.sonar.db.DbSession; import org.sonar.db.DbTester; import org.sonar.db.component.BranchDto; import org.sonar.db.component.ComponentDto; +import org.sonar.db.newcodeperiod.NewCodePeriodDto; import org.sonar.db.project.ProjectDto; import org.sonar.db.user.UserDto; import org.sonar.server.component.ComponentUpdater; @@ -59,12 +63,18 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.sonar.db.component.BranchDto.DEFAULT_MAIN_BRANCH_NAME; +import static org.sonar.db.newcodeperiod.NewCodePeriodType.NUMBER_OF_DAYS; +import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH; import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; +import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.NEW_CODE_PERIOD_TYPE_DESCRIPTION_PROJECT_CREATION; +import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.NEW_CODE_PERIOD_VALUE_DESCRIPTION_PROJECT_CREATION; import static org.sonar.server.project.Visibility.PRIVATE; import static org.sonar.test.JsonAssert.assertJson; import static org.sonarqube.ws.client.WsRequest.Method.POST; import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_MAIN_BRANCH; import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NAME; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NEW_CODE_DEFINITION_TYPE; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NEW_CODE_DEFINITION_VALUE; import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECT; import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_VISIBILITY; @@ -87,12 +97,14 @@ public class CreateActionIT { private final ProjectDefaultVisibility projectDefaultVisibility = mock(ProjectDefaultVisibility.class); private final TestProjectIndexers projectIndexers = new TestProjectIndexers(); private final PermissionTemplateService permissionTemplateService = mock(PermissionTemplateService.class); + + private PlatformEditionProvider editionProvider = mock(PlatformEditionProvider.class); private final WsActionTester ws = new WsActionTester( new CreateAction( db.getDbClient(), userSession, new ComponentUpdater(db.getDbClient(), i18n, system2, permissionTemplateService, new FavoriteUpdater(db.getDbClient()), projectIndexers, new SequenceUuidFactory(), defaultBranchNameResolver, true), - projectDefaultVisibility)); + projectDefaultVisibility, editionProvider, defaultBranchNameResolver)); @Before public void before() { @@ -208,7 +220,8 @@ public class CreateActionIT { public void do_not_add_project_to_user_favorites_if_project_creator_is_defined_in_permission_template_and_already_100_favorites() { UserDto user = db.users().insertUser(); when(permissionTemplateService.hasDefaultTemplateWithPermissionOnProjectCreator(any(DbSession.class), any(ComponentDto.class))).thenReturn(true); - rangeClosed(1, 100).forEach(i -> db.favorites().add(db.components().insertPrivateProject().getProjectDto(), user.getUuid(), user.getLogin())); + rangeClosed(1, 100).forEach(i -> db.favorites().add(db.components().insertPrivateProject().getProjectDto(), user.getUuid(), + user.getLogin())); userSession.logIn(user).addPermission(PROVISION_PROJECTS); ws.newRequest() @@ -243,14 +256,15 @@ public class CreateActionIT { .build(); assertThatThrownBy(() -> call(request)) .isInstanceOf(BadRequestException.class) - .hasMessage("Malformed key for Project: 'project%Key'. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit."); + .hasMessage("Malformed key for Project: 'project%Key'. Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least " + + "one non-digit."); } @Test public void fail_when_missing_project_parameter() { userSession.addPermission(PROVISION_PROJECTS); - assertThatThrownBy(() -> call(null, DEFAULT_PROJECT_NAME, null)) + assertThatThrownBy(() -> call(null, DEFAULT_PROJECT_NAME, null, null, null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("The 'project' parameter is missing"); } @@ -259,7 +273,7 @@ public class CreateActionIT { public void fail_when_missing_name_parameter() { userSession.addPermission(PROVISION_PROJECTS); - assertThatThrownBy(() -> call(DEFAULT_PROJECT_KEY, null, null)) + assertThatThrownBy(() -> call(DEFAULT_PROJECT_KEY, null, null, null, null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("The 'name' parameter is missing"); } @@ -296,7 +310,10 @@ public class CreateActionIT { assertThat(definition.params()).extracting(WebService.Param::key).containsExactlyInAnyOrder( PARAM_VISIBILITY, PARAM_NAME, - PARAM_PROJECT, PARAM_MAIN_BRANCH); + PARAM_PROJECT, + PARAM_MAIN_BRANCH, + PARAM_NEW_CODE_DEFINITION_TYPE, + PARAM_NEW_CODE_DEFINITION_VALUE); WebService.Param visibilityParam = definition.param(PARAM_VISIBILITY); assertThat(visibilityParam.description()).isNotEmpty(); @@ -312,6 +329,15 @@ public class CreateActionIT { WebService.Param name = definition.param(PARAM_NAME); assertThat(name.isRequired()).isTrue(); assertThat(name.description()).isEqualTo("Name of the project. If name is longer than 500, it is abbreviated."); + + WebService.Param ncdType = definition.param(PARAM_NEW_CODE_DEFINITION_TYPE); + assertThat(ncdType.isRequired()).isFalse(); + assertThat(ncdType.description()).isEqualTo(NEW_CODE_PERIOD_TYPE_DESCRIPTION_PROJECT_CREATION); + + WebService.Param ncdValue = definition.param(PARAM_NEW_CODE_DEFINITION_VALUE); + assertThat(ncdValue.isRequired()).isFalse(); + assertThat(ncdValue.description()).isEqualTo(NEW_CODE_PERIOD_VALUE_DESCRIPTION_PROJECT_CREATION); + } @Test @@ -349,16 +375,207 @@ public class CreateActionIT { .isInstanceOf(NullPointerException.class); } + @Test + public void set_default_branch_name_for_reference_branch_NCD_when_no_main_branch_provided() { + userSession.addPermission(PROVISION_PROJECTS); + + String otherBranchName = "otherBranchName"; + + when(defaultBranchNameResolver.getEffectiveMainBranchName()).thenReturn(otherBranchName); + + call(CreateRequest.builder() + .setProjectKey(DEFAULT_PROJECT_KEY) + .setName(DEFAULT_PROJECT_NAME) + .setNewCodeDefinitionType(REFERENCE_BRANCH.name()) + .build()); + + ComponentDto component = db.getDbClient().componentDao().selectByKey(db.getSession(), DEFAULT_PROJECT_KEY).get(); + + assertThat(db.getDbClient().newCodePeriodDao().selectByProject(db.getSession(), component.uuid())) + .isPresent() + .get() + .extracting(NewCodePeriodDto::getType, NewCodePeriodDto::getValue) + .containsExactly(REFERENCE_BRANCH, otherBranchName); + } + + @Test + public void set_main_branch_name_for_reference_branch_NCD() { + userSession.addPermission(PROVISION_PROJECTS); + + call(CreateRequest.builder() + .setProjectKey(DEFAULT_PROJECT_KEY) + .setName(DEFAULT_PROJECT_NAME) + .setMainBranchKey(MAIN_BRANCH) + .setNewCodeDefinitionType(REFERENCE_BRANCH.name()) + .build()); + + ComponentDto component = db.getDbClient().componentDao().selectByKey(db.getSession(), DEFAULT_PROJECT_KEY).get(); + + assertThat(db.getDbClient().newCodePeriodDao().selectByProject(db.getSession(), component.uuid())) + .isPresent() + .get() + .extracting(NewCodePeriodDto::getType, NewCodePeriodDto::getValue) + .containsExactly(REFERENCE_BRANCH, MAIN_BRANCH); + } + @Test + public void set_new_code_definition_on_project_creation() { + userSession.addPermission(PROVISION_PROJECTS); + + call(CreateRequest.builder() + .setProjectKey(DEFAULT_PROJECT_KEY) + .setName(DEFAULT_PROJECT_NAME) + .setMainBranchKey(MAIN_BRANCH) + .setNewCodeDefinitionType(NUMBER_OF_DAYS.name()) + .setNewCodeDefinitionValue("30") + .build()); + + ComponentDto component = db.getDbClient().componentDao().selectByKey(db.getSession(), DEFAULT_PROJECT_KEY).get(); + + assertThat(db.getDbClient().newCodePeriodDao().selectByProject(db.getSession(), component.uuid())) + .isPresent() + .get() + .extracting(NewCodePeriodDto::getType, NewCodePeriodDto::getValue) + .containsExactly(NUMBER_OF_DAYS, "30"); + } + + @Test + public void set_new_code_definition_branch_for_community_edition() { + when(editionProvider.get()).thenReturn(Optional.of(EditionProvider.Edition.COMMUNITY)); + + userSession.addPermission(PROVISION_PROJECTS); + + call(CreateRequest.builder() + .setProjectKey(DEFAULT_PROJECT_KEY) + .setName(DEFAULT_PROJECT_NAME) + .setMainBranchKey(MAIN_BRANCH) + .setNewCodeDefinitionType(NUMBER_OF_DAYS.name()) + .setNewCodeDefinitionValue("30") + .build()); + + ComponentDto component = db.getDbClient().componentDao().selectByKey(db.getSession(), DEFAULT_PROJECT_KEY).get(); + + assertThat(db.getDbClient().newCodePeriodDao().selectByBranch(db.getSession(), component.uuid(), component.uuid())) + .isPresent() + .get() + .extracting(NewCodePeriodDto::getType, NewCodePeriodDto::getValue, NewCodePeriodDto::getBranchUuid) + .containsExactly(NUMBER_OF_DAYS, "30", component.uuid()); + } + + @Test + public void throw_IAE_if_setting_is_not_cayc_compliant() { + userSession.addPermission(PROVISION_PROJECTS); + + CreateRequest request = CreateRequest.builder() + .setProjectKey(DEFAULT_PROJECT_KEY) + .setName(DEFAULT_PROJECT_NAME) + .setMainBranchKey(MAIN_BRANCH) + .setNewCodeDefinitionType(NUMBER_OF_DAYS.name()) + .setNewCodeDefinitionValue("99") + .build(); + + assertThatThrownBy(() -> call(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Failed to set the New Code Definition. The given value is not compatible with the Clean as You Code methodology. " + + "Please refer to the documentation for compliant options."); + assertThat(db.getDbClient().componentDao().selectByKey(db.getSession(), DEFAULT_PROJECT_KEY)) + .isEmpty(); + } + + @Test + public void throw_IAE_if_setting_is_new_code_definition_value_provided_without_type() { + userSession.addPermission(PROVISION_PROJECTS); + + CreateRequest request = CreateRequest.builder() + .setProjectKey(DEFAULT_PROJECT_KEY) + .setName(DEFAULT_PROJECT_NAME) + .setMainBranchKey(MAIN_BRANCH) + .setNewCodeDefinitionValue("99") + .build(); + + assertThatThrownBy(() -> call(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("New code definition type is required when new code definition value is provided"); + assertThat(db.getDbClient().componentDao().selectByKey(db.getSession(), DEFAULT_PROJECT_KEY)) + .isEmpty(); + assertThat(db.getDbClient().newCodePeriodDao().selectAll(db.getSession())).isEmpty(); + } + + @Test + public void set_new_code_definition_for_project_for_developer_edition() { + when(editionProvider.get()).thenReturn(Optional.of(EditionProvider.Edition.DEVELOPER)); + + userSession.addPermission(PROVISION_PROJECTS); + + call(CreateRequest.builder() + .setProjectKey(DEFAULT_PROJECT_KEY) + .setName(DEFAULT_PROJECT_NAME) + .setMainBranchKey(MAIN_BRANCH) + .setNewCodeDefinitionType(NUMBER_OF_DAYS.name()) + .setNewCodeDefinitionValue("30") + .build()); + + ComponentDto component = db.getDbClient().componentDao().selectByKey(db.getSession(), DEFAULT_PROJECT_KEY).get(); + + assertThat(db.getDbClient().newCodePeriodDao().selectByProject(db.getSession(), component.uuid())) + .isPresent() + .get() + .extracting(NewCodePeriodDto::getType, NewCodePeriodDto::getValue, NewCodePeriodDto::getBranchUuid) + .containsExactly(NUMBER_OF_DAYS, "30", null); + } + + + @Test + public void do_not_create_project_when_ncdType_invalid() { + when(editionProvider.get()).thenReturn(Optional.of(EditionProvider.Edition.DEVELOPER)); + + userSession.addPermission(PROVISION_PROJECTS); + + CreateRequest request = CreateRequest.builder() + .setProjectKey(DEFAULT_PROJECT_KEY) + .setName(DEFAULT_PROJECT_NAME) + .setMainBranchKey(MAIN_BRANCH) + .setNewCodeDefinitionType("InvalidType") + .build(); + + assertThatThrownBy(() -> call(request)) + .isInstanceOf(IllegalArgumentException.class); + + assertThat(db.getDbClient().componentDao().selectByKey(db.getSession(), DEFAULT_PROJECT_KEY)) + .isEmpty(); + + } + + @Test + public void do_not_set_new_code_definition_when_ncdType_not_provided() { + when(editionProvider.get()).thenReturn(Optional.of(EditionProvider.Edition.DEVELOPER)); + + userSession.addPermission(PROVISION_PROJECTS); + + call(CreateRequest.builder() + .setProjectKey(DEFAULT_PROJECT_KEY) + .setName(DEFAULT_PROJECT_NAME) + .setMainBranchKey(MAIN_BRANCH) + .build()); + + ComponentDto component = db.getDbClient().componentDao().selectByKey(db.getSession(), DEFAULT_PROJECT_KEY).get(); + + assertThat(db.getDbClient().newCodePeriodDao().selectByProject(db.getSession(), component.uuid())) + .isEmpty(); + } + private CreateWsResponse call(CreateRequest request) { - return call(request.getProjectKey(), request.getName(), request.getMainBranchKey()); + return call(request.getProjectKey(), request.getName(), request.getMainBranchKey(), request.getNewCodeDefinitionType(), request.getNewCodeDefinitionValue()); } - private CreateWsResponse call(@Nullable String projectKey, @Nullable String projectName, @Nullable String mainBranch) { + private CreateWsResponse call(@Nullable String projectKey, @Nullable String projectName, @Nullable String mainBranch, + @Nullable String newCodeDefinitionType, @Nullable String newCodeDefinitionValue) { TestRequest httpRequest = ws.newRequest() .setMethod(POST.name()); ofNullable(projectKey).ifPresent(key -> httpRequest.setParam("project", key)); ofNullable(projectName).ifPresent(name -> httpRequest.setParam("name", name)); ofNullable(mainBranch).ifPresent(name -> httpRequest.setParam("mainBranch", mainBranch)); + ofNullable(newCodeDefinitionType).ifPresent(type -> httpRequest.setParam(PARAM_NEW_CODE_DEFINITION_TYPE, type)); + ofNullable(newCodeDefinitionValue).ifPresent(value -> httpRequest.setParam(PARAM_NEW_CODE_DEFINITION_VALUE, value)); return httpRequest.executeProtobuf(CreateWsResponse.class); } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/newcodeperiod/NewCodePeriodUtils.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/newcodeperiod/NewCodePeriodUtils.java new file mode 100644 index 00000000000..0b5703841cd --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/newcodeperiod/NewCodePeriodUtils.java @@ -0,0 +1,190 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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.newcodeperiod; + +import com.google.common.base.Preconditions; +import java.util.EnumSet; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; +import javax.annotation.Nullable; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.BranchDto; +import org.sonar.db.component.SnapshotDto; +import org.sonar.db.newcodeperiod.NewCodePeriodParser; +import org.sonar.db.newcodeperiod.NewCodePeriodType; +import org.sonar.db.project.ProjectDto; +import org.sonar.server.exceptions.NotFoundException; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.String.format; +import static org.sonar.db.newcodeperiod.NewCodePeriodType.NUMBER_OF_DAYS; +import static org.sonar.db.newcodeperiod.NewCodePeriodType.PREVIOUS_VERSION; +import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH; +import static org.sonar.db.newcodeperiod.NewCodePeriodType.SPECIFIC_ANALYSIS; + +public interface NewCodePeriodUtils { + String BEGIN_LIST = ""; + String BEGIN_ITEM_LIST = "
  • "; + String END_ITEM_LIST = "
  • "; + String NEW_CODE_PERIOD_TYPE_DESCRIPTION = "Type
    " + + "New code definitions of the following types are allowed:" + + BEGIN_LIST + + BEGIN_ITEM_LIST + SPECIFIC_ANALYSIS.name() + " - can be set at branch level only" + END_ITEM_LIST + + BEGIN_ITEM_LIST + PREVIOUS_VERSION.name() + " - can be set at any level (global, project, branch)" + END_ITEM_LIST + + BEGIN_ITEM_LIST + NUMBER_OF_DAYS.name() + " - can be set at any level (global, project, branch)" + END_ITEM_LIST + + BEGIN_ITEM_LIST + REFERENCE_BRANCH.name() + " - can only be set for projects and branches" + END_ITEM_LIST + + END_LIST; + + String NEW_CODE_PERIOD_TYPE_DESCRIPTION_PROJECT_CREATION = "Type
    " + + "New code definitions of the following types are allowed:" + + BEGIN_LIST + + BEGIN_ITEM_LIST + PREVIOUS_VERSION.name() + END_ITEM_LIST + + BEGIN_ITEM_LIST + NUMBER_OF_DAYS.name() + END_ITEM_LIST + + BEGIN_ITEM_LIST + REFERENCE_BRANCH.name() + " - will default to the main branch. A new code definition should be set for the branch itself. " + + "Until you do so, this branch will use itself as a reference branch and no code will be considered new for this branch" + END_ITEM_LIST + + END_LIST; + + String NEW_CODE_PERIOD_VALUE_DESCRIPTION = "Value
    " + + "For each type, a different value is expected:" + + BEGIN_LIST + + BEGIN_ITEM_LIST + "the uuid of an analysis, when type is " + SPECIFIC_ANALYSIS.name() + END_ITEM_LIST + + BEGIN_ITEM_LIST + "no value, when type is " + PREVIOUS_VERSION.name() + END_ITEM_LIST + + BEGIN_ITEM_LIST + "a number between 1 and 90, when type is " + NUMBER_OF_DAYS.name() + END_ITEM_LIST + + BEGIN_ITEM_LIST + "a string, when type is " + REFERENCE_BRANCH.name() + END_ITEM_LIST + + END_LIST; + String NEW_CODE_PERIOD_VALUE_DESCRIPTION_PROJECT_CREATION = "Value
    " + + "For each type, a different value is expected:" + + BEGIN_LIST + + BEGIN_ITEM_LIST + "no value, when type is " + PREVIOUS_VERSION.name() + " and " + REFERENCE_BRANCH.name() + END_ITEM_LIST + + BEGIN_ITEM_LIST + "a number between 1 and 90, when type is " + NUMBER_OF_DAYS.name() + END_ITEM_LIST + + END_LIST; + + String UNEXPECTED_VALUE_ERROR_MESSAGE = "Unexpected value for type '%s'"; + + static Optional getNewCodeDefinitionValue(DbSession dbSession, DbClient dbClient, NewCodePeriodType type, @Nullable ProjectDto project, + @Nullable BranchDto branch, @Nullable String value) { + switch (type) { + case PREVIOUS_VERSION: + Preconditions.checkArgument(value == null, UNEXPECTED_VALUE_ERROR_MESSAGE, type); + return Optional.empty(); + case NUMBER_OF_DAYS: + requireValue(type, value); + return Optional.of(parseDays(value)); + case SPECIFIC_ANALYSIS: + requireValue(type, value); + requireBranch(type, branch); + SnapshotDto analysis = getAnalysis(dbSession, value, project, branch, dbClient); + return Optional.of(analysis.getUuid()); + case REFERENCE_BRANCH: + requireValue(type, value); + return Optional.of(value); + default: + throw new IllegalStateException("Unexpected type: " + type); + } + } + + static Optional getNewCodeDefinitionValueProjectCreation(NewCodePeriodType type, @Nullable String value, String defaultBranchName) { + switch (type) { + case PREVIOUS_VERSION: + Preconditions.checkArgument(value == null, UNEXPECTED_VALUE_ERROR_MESSAGE, type); + return Optional.empty(); + case NUMBER_OF_DAYS: + requireValue(type, value); + return Optional.of(parseDays(value)); + case REFERENCE_BRANCH: + Preconditions.checkArgument(value == null, UNEXPECTED_VALUE_ERROR_MESSAGE, type); + return Optional.of(defaultBranchName); + default: + throw new IllegalStateException("Unexpected type: " + type); + } + } + + private static String parseDays(String value) { + try { + return Integer.toString(NewCodePeriodParser.parseDays(value)); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to parse number of days: " + value); + } + } + + private static void requireValue(NewCodePeriodType type, @Nullable String value) { + Preconditions.checkArgument(value != null, "New code definition type '%s' requires a value", type); + } + + private static void requireBranch(NewCodePeriodType type, @Nullable BranchDto branch) { + Preconditions.checkArgument(branch != null, "New code definition type '%s' requires a branch", type); + } + private static Set getInstanceTypes() { + return EnumSet.of(PREVIOUS_VERSION, NUMBER_OF_DAYS); + } + + private static Set getProjectTypes() { + return EnumSet.of(PREVIOUS_VERSION, NUMBER_OF_DAYS, REFERENCE_BRANCH); + } + + private static Set getBranchTypes() { + return EnumSet.of(PREVIOUS_VERSION, NUMBER_OF_DAYS, SPECIFIC_ANALYSIS, REFERENCE_BRANCH); + } + + static NewCodePeriodType validateType(String typeStr, boolean isOverall, boolean isBranch) { + NewCodePeriodType type; + try { + type = NewCodePeriodType.valueOf(typeStr.toUpperCase(Locale.US)); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid type: " + typeStr); + } + + if (isOverall) { + checkType("Overall setting", getInstanceTypes(), type); + } else if (isBranch) { + checkType("Branches", getBranchTypes(), type); + } else { + checkType("Projects", getProjectTypes(), type); + } + return type; + } + + private static SnapshotDto getAnalysis(DbSession dbSession, String analysisUuid, ProjectDto project, BranchDto branch, DbClient dbClient) { + SnapshotDto snapshotDto = dbClient.snapshotDao().selectByUuid(dbSession, analysisUuid) + .orElseThrow(() -> new NotFoundException(format("Analysis '%s' is not found", analysisUuid))); + checkAnalysis(dbSession, project, branch, snapshotDto, dbClient); + return snapshotDto; + } + + private static void checkAnalysis(DbSession dbSession, ProjectDto project, BranchDto branch, SnapshotDto analysis, DbClient dbClient) { + BranchDto analysisBranch = dbClient.branchDao().selectByUuid(dbSession, analysis.getComponentUuid()).orElse(null); + boolean analysisMatchesProjectBranch = analysisBranch != null && analysisBranch.getUuid().equals(branch.getUuid()); + + checkArgument(analysisMatchesProjectBranch, + "Analysis '%s' does not belong to branch '%s' of project '%s'", + analysis.getUuid(), branch.getKey(), project.getKey()); + } + + private static void checkType(String name, Set validTypes, NewCodePeriodType type) { + if (!validTypes.contains(type)) { + throw new IllegalArgumentException(String.format("Invalid type '%s'. %s can only be set with types: %s", type, name, validTypes)); + } + } + +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/newcodeperiod/ws/SetAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/newcodeperiod/ws/SetAction.java index c3a29aecd35..899398d7651 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/newcodeperiod/ws/SetAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/newcodeperiod/ws/SetAction.java @@ -19,11 +19,8 @@ */ package org.sonar.server.newcodeperiod.ws; -import com.google.common.base.Preconditions; import java.util.EnumSet; -import java.util.Locale; import java.util.Set; -import javax.annotation.Nullable; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; @@ -34,10 +31,8 @@ import org.sonar.core.platform.PlatformEditionProvider; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.component.BranchDto; -import org.sonar.db.component.SnapshotDto; import org.sonar.db.newcodeperiod.NewCodePeriodDao; import org.sonar.db.newcodeperiod.NewCodePeriodDto; -import org.sonar.db.newcodeperiod.NewCodePeriodParser; import org.sonar.db.newcodeperiod.NewCodePeriodType; import org.sonar.db.project.ProjectDto; import org.sonar.server.component.ComponentFinder; @@ -45,12 +40,15 @@ import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.newcodeperiod.CaycUtils; import org.sonar.server.user.UserSession; -import static com.google.common.base.Preconditions.checkArgument; import static java.lang.String.format; import static org.sonar.db.newcodeperiod.NewCodePeriodType.NUMBER_OF_DAYS; import static org.sonar.db.newcodeperiod.NewCodePeriodType.PREVIOUS_VERSION; import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH; import static org.sonar.db.newcodeperiod.NewCodePeriodType.SPECIFIC_ANALYSIS; +import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.NEW_CODE_PERIOD_TYPE_DESCRIPTION; +import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.NEW_CODE_PERIOD_VALUE_DESCRIPTION; +import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.getNewCodeDefinitionValue; +import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.validateType; import static org.sonar.server.ws.WsUtils.createHtmlExternalLink; public class SetAction implements NewCodePeriodsWsAction { @@ -111,25 +109,9 @@ public class SetAction implements NewCodePeriodsWsAction { .setDescription("Branch key"); action.createParam(PARAM_TYPE) .setRequired(true) - .setDescription("Type
    " + - "New code definitions of the following types are allowed:" + - BEGIN_LIST + - BEGIN_ITEM_LIST + SPECIFIC_ANALYSIS.name() + " - can be set at branch level only" + END_ITEM_LIST + - BEGIN_ITEM_LIST + PREVIOUS_VERSION.name() + " - can be set at any level (global, project, branch)" + END_ITEM_LIST + - BEGIN_ITEM_LIST + NUMBER_OF_DAYS.name() + " - can be set at any level (global, project, branch)" + END_ITEM_LIST + - BEGIN_ITEM_LIST + REFERENCE_BRANCH.name() + " - can only be set for projects and branches" + END_ITEM_LIST + - END_LIST - ); + .setDescription(NEW_CODE_PERIOD_VALUE_DESCRIPTION); action.createParam(PARAM_VALUE) - .setDescription("Value
    " + - "For each type, a different value is expected:" + - BEGIN_LIST + - BEGIN_ITEM_LIST + "the uuid of an analysis, when type is " + SPECIFIC_ANALYSIS.name() + END_ITEM_LIST + - BEGIN_ITEM_LIST + "no value, when type is " + PREVIOUS_VERSION.name() + END_ITEM_LIST + - BEGIN_ITEM_LIST + "a number between 1 and 90, when type is " + NUMBER_OF_DAYS.name() + END_ITEM_LIST + - BEGIN_ITEM_LIST + "a string, when type is " + REFERENCE_BRANCH.name() + END_ITEM_LIST + - END_LIST - ); + .setDescription(NEW_CODE_PERIOD_TYPE_DESCRIPTION); } @Override @@ -172,7 +154,7 @@ public class SetAction implements NewCodePeriodsWsAction { userSession.checkIsSystemAdministrator(); } - setValue(dbSession, dto, type, project, branch, valueStr); + getNewCodeDefinitionValue(dbSession, dbClient, type, project, branch, valueStr).ifPresent(dto::setValue); if (!CaycUtils.isNewCodePeriodCompliant(dto.getType(), dto.getValue())) { throw new IllegalArgumentException("Failed to set the New Code Definition. The given value is not compatible with the Clean as You Code methodology. " @@ -184,47 +166,6 @@ public class SetAction implements NewCodePeriodsWsAction { } } - private void setValue(DbSession dbSession, NewCodePeriodDto dto, NewCodePeriodType type, @Nullable ProjectDto project, - @Nullable BranchDto branch, @Nullable String value) { - switch (type) { - case PREVIOUS_VERSION: - Preconditions.checkArgument(value == null, "Unexpected value for type '%s'", type); - break; - case NUMBER_OF_DAYS: - requireValue(type, value); - dto.setValue(parseDays(value)); - break; - case SPECIFIC_ANALYSIS: - requireValue(type, value); - requireBranch(type, branch); - SnapshotDto analysis = getAnalysis(dbSession, value, project, branch); - dto.setValue(analysis.getUuid()); - break; - case REFERENCE_BRANCH: - requireValue(type, value); - dto.setValue(value); - break; - default: - throw new IllegalStateException("Unexpected type: " + type); - } - } - - private static String parseDays(String value) { - try { - return Integer.toString(NewCodePeriodParser.parseDays(value)); - } catch (Exception e) { - throw new IllegalArgumentException("Failed to parse number of days: " + value); - } - } - - private static void requireValue(NewCodePeriodType type, @Nullable String value) { - Preconditions.checkArgument(value != null, "New code definition type '%s' requires a value", type); - } - - private static void requireBranch(NewCodePeriodType type, @Nullable BranchDto branch) { - Preconditions.checkArgument(branch != null, "New code definition type '%s' requires a branch", type); - } - private BranchDto getBranch(DbSession dbSession, ProjectDto project, String branchKey) { return dbClient.branchDao().selectByBranchKey(dbSession, project.getUuid(), branchKey) .orElseThrow(() -> new NotFoundException(format("Branch '%s' in project '%s' not found", branchKey, project.getKey()))); @@ -240,44 +181,4 @@ public class SetAction implements NewCodePeriodsWsAction { .findFirst() .orElseThrow(() -> new NotFoundException(format("Main branch in project '%s' is not found", project.getKey()))); } - - private static NewCodePeriodType validateType(String typeStr, boolean isOverall, boolean isBranch) { - NewCodePeriodType type; - try { - type = NewCodePeriodType.valueOf(typeStr.toUpperCase(Locale.US)); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid type: " + typeStr); - } - - if (isOverall) { - checkType("Overall setting", OVERALL_TYPES, type); - } else if (isBranch) { - checkType("Branches", BRANCH_TYPES, type); - } else { - checkType("Projects", PROJECT_TYPES, type); - } - return type; - } - - private SnapshotDto getAnalysis(DbSession dbSession, String analysisUuid, ProjectDto project, BranchDto branch) { - SnapshotDto snapshotDto = dbClient.snapshotDao().selectByUuid(dbSession, analysisUuid) - .orElseThrow(() -> new NotFoundException(format("Analysis '%s' is not found", analysisUuid))); - checkAnalysis(dbSession, project, branch, snapshotDto); - return snapshotDto; - } - - private void checkAnalysis(DbSession dbSession, ProjectDto project, BranchDto branch, SnapshotDto analysis) { - BranchDto analysisBranch = dbClient.branchDao().selectByUuid(dbSession, analysis.getComponentUuid()).orElse(null); - boolean analysisMatchesProjectBranch = analysisBranch != null && analysisBranch.getUuid().equals(branch.getUuid()); - - checkArgument(analysisMatchesProjectBranch, - "Analysis '%s' does not belong to branch '%s' of project '%s'", - analysis.getUuid(), branch.getKey(), project.getKey()); - } - - private static void checkType(String name, Set validTypes, NewCodePeriodType type) { - if (!validTypes.contains(type)) { - throw new IllegalArgumentException(String.format("Invalid type '%s'. %s can only be set with types: %s", type, name, validTypes)); - } - } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/project/ws/CreateAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/project/ws/CreateAction.java index 69a440fe38b..06278fd680d 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/project/ws/CreateAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/project/ws/CreateAction.java @@ -19,16 +19,23 @@ */ package org.sonar.server.project.ws; +import java.util.Optional; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonar.api.server.ws.Change; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; +import org.sonar.core.platform.EditionProvider; +import org.sonar.core.platform.PlatformEditionProvider; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.component.ComponentDto; +import org.sonar.db.newcodeperiod.NewCodePeriodDto; +import org.sonar.db.newcodeperiod.NewCodePeriodType; import org.sonar.server.component.ComponentUpdater; +import org.sonar.server.newcodeperiod.CaycUtils; +import org.sonar.server.project.DefaultBranchNameResolver; import org.sonar.server.project.ProjectDefaultVisibility; import org.sonar.server.project.Visibility; import org.sonar.server.user.UserSession; @@ -41,11 +48,17 @@ import static org.sonar.core.component.ComponentKeys.MAX_COMPONENT_KEY_LENGTH; import static org.sonar.db.component.ComponentValidator.MAX_COMPONENT_NAME_LENGTH; import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; import static org.sonar.server.component.NewComponent.newComponentBuilder; +import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.NEW_CODE_PERIOD_TYPE_DESCRIPTION_PROJECT_CREATION; +import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.NEW_CODE_PERIOD_VALUE_DESCRIPTION_PROJECT_CREATION; +import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.getNewCodeDefinitionValueProjectCreation; +import static org.sonar.server.newcodeperiod.NewCodePeriodUtils.validateType; import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; import static org.sonar.server.ws.WsUtils.writeProtobuf; import static org.sonarqube.ws.client.project.ProjectsWsParameters.ACTION_CREATE; import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_MAIN_BRANCH; import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NAME; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NEW_CODE_DEFINITION_TYPE; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NEW_CODE_DEFINITION_VALUE; import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECT; import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_VISIBILITY; @@ -55,13 +68,18 @@ public class CreateAction implements ProjectsWsAction { private final UserSession userSession; private final ComponentUpdater componentUpdater; private final ProjectDefaultVisibility projectDefaultVisibility; + private final PlatformEditionProvider editionProvider; + private final DefaultBranchNameResolver defaultBranchNameResolver; public CreateAction(DbClient dbClient, UserSession userSession, ComponentUpdater componentUpdater, - ProjectDefaultVisibility projectDefaultVisibility) { + ProjectDefaultVisibility projectDefaultVisibility, PlatformEditionProvider editionProvider, + DefaultBranchNameResolver defaultBranchNameResolver) { this.dbClient = dbClient; this.userSession = userSession; this.componentUpdater = componentUpdater; this.projectDefaultVisibility = projectDefaultVisibility; + this.editionProvider = editionProvider; + this.defaultBranchNameResolver = defaultBranchNameResolver; } @Override @@ -91,17 +109,23 @@ public class CreateAction implements ProjectsWsAction { action.createParam(PARAM_MAIN_BRANCH) .setDescription("Key of the main branch of the project. If not provided, the default main branch key will be used.") - .setRequired(false) .setSince("9.8") .setExampleValue("develop"); action.createParam(PARAM_VISIBILITY) .setDescription("Whether the created project should be visible to everyone, or only specific user/groups.
    " + "If no visibility is specified, the default project visibility will be used.") - .setRequired(false) .setSince("6.4") .setPossibleValues(Visibility.getLabels()); + action.createParam(PARAM_NEW_CODE_DEFINITION_TYPE) + .setDescription(NEW_CODE_PERIOD_TYPE_DESCRIPTION_PROJECT_CREATION) + .setSince("10.1"); + + action.createParam(PARAM_NEW_CODE_DEFINITION_VALUE) + .setDescription(NEW_CODE_PERIOD_VALUE_DESCRIPTION_PROJECT_CREATION) + .setSince("10.1"); + } @Override @@ -113,28 +137,69 @@ public class CreateAction implements ProjectsWsAction { private CreateWsResponse doHandle(CreateRequest request) { try (DbSession dbSession = dbClient.openSession(false)) { userSession.checkPermission(PROVISION_PROJECTS); - String visibility = request.getVisibility(); - boolean changeToPrivate = visibility == null ? projectDefaultVisibility.get(dbSession).isPrivate() : "private".equals(visibility); - - ComponentDto componentDto = componentUpdater.create(dbSession, newComponentBuilder() - .setKey(request.getProjectKey()) - .setName(request.getName()) - .setPrivate(changeToPrivate) - .setQualifier(PROJECT) - .build(), - userSession.isLoggedIn() ? userSession.getUuid() : null, - userSession.isLoggedIn() ? userSession.getLogin() : null, - request.getMainBranchKey()).mainBranchComponent(); + checkNewCodeDefinitionParam(request); + ComponentDto componentDto = createProject(request, dbSession); + if(request.getNewCodeDefinitionType() != null) { + createNewCodeDefinition(dbSession, request, componentDto.uuid()); + } + componentUpdater.commitAndIndex(dbSession, componentDto); return toCreateResponse(componentDto); } } + private static void checkNewCodeDefinitionParam(CreateRequest request) { + if (request.getNewCodeDefinitionType() == null && request.getNewCodeDefinitionValue() != null) { + throw new IllegalArgumentException("New code definition type is required when new code definition value is provided"); + } + } + private ComponentDto createProject(CreateRequest request, DbSession dbSession) { + String visibility = request.getVisibility(); + boolean changeToPrivate = visibility == null ? projectDefaultVisibility.get(dbSession).isPrivate() : "private".equals(visibility); + + return componentUpdater.createWithoutCommit(dbSession, newComponentBuilder() + .setKey(request.getProjectKey()) + .setName(request.getName()) + .setPrivate(changeToPrivate) + .setQualifier(PROJECT) + .build(), + userSession.isLoggedIn() ? userSession.getUuid() : null, + userSession.isLoggedIn() ? userSession.getLogin() : null, + request.getMainBranchKey(), s -> {}).mainBranchComponent(); + + } + private void createNewCodeDefinition(DbSession dbSession, CreateRequest request, String projectUuid) { + + boolean isCommunityEdition = editionProvider.get().filter(EditionProvider.Edition.COMMUNITY::equals).isPresent(); + NewCodePeriodType newCodePeriodType = validateType(request.getNewCodeDefinitionType(), false, isCommunityEdition); + String newCodePeriodValue = request.getNewCodeDefinitionValue(); + String defaultBranchName = Optional.ofNullable(request.getMainBranchKey()).orElse(defaultBranchNameResolver.getEffectiveMainBranchName()); + + NewCodePeriodDto dto = new NewCodePeriodDto(); + dto.setType(newCodePeriodType); + dto.setProjectUuid(projectUuid); + + if (isCommunityEdition) { + dto.setBranchUuid(projectUuid); + } + + getNewCodeDefinitionValueProjectCreation(newCodePeriodType, newCodePeriodValue, defaultBranchName).ifPresent(dto::setValue); + + if (!CaycUtils.isNewCodePeriodCompliant(dto.getType(), dto.getValue())) { + throw new IllegalArgumentException("Failed to set the New Code Definition. The given value is not compatible with the Clean as You Code methodology. " + + "Please refer to the documentation for compliant options."); + } + + dbClient.newCodePeriodDao().insert(dbSession, dto); + } + private static CreateRequest toCreateRequest(Request request) { return CreateRequest.builder() .setProjectKey(request.mandatoryParam(PARAM_PROJECT)) .setName(abbreviate(request.mandatoryParam(PARAM_NAME), MAX_COMPONENT_NAME_LENGTH)) .setVisibility(request.param(PARAM_VISIBILITY)) .setMainBranchKey(request.param(PARAM_MAIN_BRANCH)) + .setNewCodeDefinitionType(request.param(PARAM_NEW_CODE_DEFINITION_TYPE)) + .setNewCodeDefinitionValue(request.param(PARAM_NEW_CODE_DEFINITION_VALUE)) .build(); } @@ -155,11 +220,19 @@ public class CreateAction implements ProjectsWsAction { @CheckForNull private final String visibility; + @CheckForNull + private final String newCodeDefinitionType; + + @CheckForNull + private final String newCodeDefinitionValue; + private CreateRequest(Builder builder) { this.projectKey = builder.projectKey; this.name = builder.name; this.visibility = builder.visibility; this.mainBranchKey = builder.mainBranchKey; + this.newCodeDefinitionType = builder.newCodeDefinitionType; + this.newCodeDefinitionValue = builder.newCodeDefinitionValue; } public String getProjectKey() { @@ -179,6 +252,16 @@ public class CreateAction implements ProjectsWsAction { return mainBranchKey; } + @CheckForNull + public String getNewCodeDefinitionType() { + return newCodeDefinitionType; + } + + @CheckForNull + public String getNewCodeDefinitionValue() { + return newCodeDefinitionValue; + } + public static Builder builder() { return new Builder(); } @@ -191,6 +274,11 @@ public class CreateAction implements ProjectsWsAction { @CheckForNull private String visibility; + @CheckForNull + private String newCodeDefinitionType; + + @CheckForNull + private String newCodeDefinitionValue; private Builder() { } @@ -217,6 +305,16 @@ public class CreateAction implements ProjectsWsAction { return this; } + public Builder setNewCodeDefinitionType(@Nullable String newCodeDefinitionType) { + this.newCodeDefinitionType = newCodeDefinitionType; + return this; + } + + public Builder setNewCodeDefinitionValue(@Nullable String newCodeDefinitionValue) { + this.newCodeDefinitionValue = newCodeDefinitionValue; + return this; + } + public CreateRequest build() { requireNonNull(projectKey); requireNonNull(name); diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsWsParameters.java index 93c6f3aef7e..74801f4c935 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsWsParameters.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsWsParameters.java @@ -46,11 +46,14 @@ public class ProjectsWsParameters { public static final String PARAM_PROJECTS = "projects"; public static final String PARAM_ALM_ID = "almId"; public static final String PARAM_ALM_REPOSITORY_ID = "almRepoId"; + public static final String PARAM_NEW_CODE_DEFINITION_VALUE = "newCodeDefinitionValue"; + public static final String PARAM_NEW_CODE_DEFINITION_TYPE = "newCodeDefinitionType"; public static final String FILTER_LANGUAGES = "languages"; public static final String FILTER_TAGS = "tags"; public static final String FILTER_QUALIFIER = "qualifier"; + private ProjectsWsParameters() { // static utils only } -- 2.39.5