From 120f4b533683e227e23afff96d8903dde121c105 Mon Sep 17 00:00:00 2001 From: Teryk Bellahsene Date: Mon, 27 Feb 2017 16:39:45 +0100 Subject: [PATCH] SONAR-8838 Create WS api/project_tags/set to set tags of a project --- .../projecttag/ws/ProjectTagsWsModule.java | 3 +- .../sonar/server/projecttag/ws/SetAction.java | 105 ++++++++++++ .../ws/ProjectTagsWsModuleTest.java | 2 +- .../server/projecttag/ws/SetActionTest.java | 161 ++++++++++++++++++ 4 files changed, 269 insertions(+), 2 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/SetAction.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/SetActionTest.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/ProjectTagsWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/ProjectTagsWsModule.java index 0991ab3442e..9d1b106ad9d 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/ProjectTagsWsModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/ProjectTagsWsModule.java @@ -26,7 +26,8 @@ public class ProjectTagsWsModule extends Module { @Override protected void configureModule() { add( - ProjectTagsWs.class + ProjectTagsWs.class, + SetAction.class ); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/SetAction.java b/server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/SetAction.java new file mode 100644 index 00000000000..dae6604ebb3 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/SetAction.java @@ -0,0 +1,105 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.projecttag.ws; + +import java.util.List; +import java.util.Locale; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.web.UserRole; +import org.sonar.core.util.stream.Collectors; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.user.UserSession; + +import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; +import static org.sonar.server.ws.WsUtils.checkRequest; + +public class SetAction implements ProjectTagsWsAction { + /** + * The characters allowed in project tags are lower-case + * letters, digits, plus (+), sharp (#), dash (-) and dot (.) + */ + private static final String VALID_TAG_REGEXP = "[a-z0-9+#\\-.]+$"; + private static final String PARAM_PROJECT = "project"; + private static final String PARAM_TAGS = "tags"; + + private final DbClient dbClient; + private final ComponentFinder componentFinder; + private final UserSession userSession; + + public SetAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) { + this.dbClient = dbClient; + this.componentFinder = componentFinder; + this.userSession = userSession; + } + + private static String checkTag(String tag) { + checkRequest(tag.matches(VALID_TAG_REGEXP), "Tag '%s' is invalid. Project tags accept only the characters: a-z, 0-9, '+', '-', '#', '.'", tag); + return tag; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("set") + .setDescription("Set tags on a project.
" + + "Requires the following permission: 'Administer' rights on the specified project") + .setSince("6.4") + .setHandler(this); + + action.createParam(PARAM_PROJECT) + .setDescription("Project key") + .setRequired(true) + .setExampleValue(KEY_PROJECT_EXAMPLE_001); + + action.createParam(PARAM_TAGS) + .setDescription("Comma-separated list of tags") + .setRequired(true) + .setExampleValue("finance, offshore"); + } + + @Override + public void handle(Request request, Response response) throws Exception { + String projectKey = request.mandatoryParam(PARAM_PROJECT); + List tags = request.mandatoryParamAsStrings(PARAM_TAGS).stream() + .filter(StringUtils::isNotBlank) + .map(t -> t.toLowerCase(Locale.ENGLISH)) + .map(SetAction::checkTag) + .distinct() + .collect(Collectors.toList()); + + try (DbSession dbSession = dbClient.openSession(false)) { + ComponentDto project = componentFinder.getByKey(dbSession, projectKey); + checkRequest(project.isRootProject(), "Component must be a project"); + userSession.checkComponentUuidPermission(UserRole.ADMIN, project.uuid()); + + project.setTags(tags); + dbClient.componentDao().updateTags(dbSession, project); + dbSession.commit(); + } + + response.noContent(); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/ProjectTagsWsModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/ProjectTagsWsModuleTest.java index 82ae8049964..35f38ba7d33 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/ProjectTagsWsModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/ProjectTagsWsModuleTest.java @@ -31,6 +31,6 @@ public class ProjectTagsWsModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new ProjectTagsWsModule().configure(container); - assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 1); + assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 2); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/SetActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/SetActionTest.java new file mode 100644 index 00000000000..4f7f83eabed --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/SetActionTest.java @@ -0,0 +1,161 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.projecttag.ws; + +import javax.annotation.Nullable; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.web.UserRole; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.exceptions.BadRequestException; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.TestResponse; +import org.sonar.server.ws.WsActionTester; + +import static java.net.HttpURLConnection.HTTP_NO_CONTENT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.core.util.Protobuf.setNullable; + +public class SetActionTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone().logIn().setRoot(); + @Rule + public DbTester db = DbTester.create(); + private DbClient dbClient = db.getDbClient(); + public WsActionTester ws = new WsActionTester(new SetAction(dbClient, new ComponentFinder(dbClient), userSession)); + private DbSession dbSession = db.getSession(); + private ComponentDto project; + + @Before + public void setUp() { + project = db.components().insertProject(); + } + + @Test + public void set_tags_exclude_empty_values() { + TestResponse response = call(project.key(), "finance , offshore, platform, ,"); + + assertTags(project.key(), "finance", "offshore", "platform"); + assertThat(response.getStatus()).isEqualTo(HTTP_NO_CONTENT); + } + + @Test + public void reset_tags() { + project = db.components().insertProject(p -> p.setTagsString("platform,scanner")); + + call(project.key(), ""); + + assertNoTags(project.key()); + } + + @Test + public void override_existing_tags() { + project = db.components().insertProject(p -> p.setTagsString("marketing,languages")); + + call(project.key(), "finance,offshore,platform"); + + assertTags(project.key(), "finance", "offshore", "platform"); + } + + @Test + public void set_tags_as_project_admin() { + userSession.logIn().addProjectUuidPermissions(UserRole.ADMIN, project.uuid()); + + call(project.key(), "platform, lambda"); + + assertTags(project.key(), "platform", "lambda"); + } + + @Test + public void do_not_duplicate_tags() { + call(project.key(), "atlas, atlas, atlas"); + + assertTags(project.key(), "atlas"); + } + + @Test + public void fail_if_tag_does_not_respect_format() { + expectedException.expect(BadRequestException.class); + expectedException.expectMessage("_finance_' is invalid. Project tags accept only the characters: a-z, 0-9, '+', '-', '#', '.'"); + + call(project.key(), "_finance_"); + } + + @Test + public void fail_if_not_project_admin() { + userSession.logIn().addProjectUuidPermissions(UserRole.USER, project.key()); + + expectedException.expect(ForbiddenException.class); + + call(project.key(), "platform"); + } + + @Test + public void fail_if_no_project() { + expectedException.expect(IllegalArgumentException.class); + + call(null, "platform"); + } + + @Test + public void fail_if_no_tags() { + expectedException.expect(IllegalArgumentException.class); + + call(project.key(), null); + } + + @Test + public void definition() { + WebService.Action definition = ws.getDef(); + + assertThat(definition.params()).extracting(WebService.Param::key) + .containsOnly("project", "tags"); + assertThat(definition.description()).isNotEmpty(); + assertThat(definition.since()).isEqualTo("6.4"); + } + + private TestResponse call(@Nullable String projectKey, @Nullable String tags) { + TestRequest request = ws.newRequest(); + setNullable(projectKey, p -> request.setParam("project", p)); + setNullable(tags, t -> request.setParam("tags", tags)); + + return request.execute(); + } + + private void assertTags(String projectKey, String... tags) { + assertThat(dbClient.componentDao().selectOrFailByKey(dbSession, projectKey).getTags()).containsExactlyInAnyOrder(tags); + } + + private void assertNoTags(String projectKey) { + assertThat(dbClient.componentDao().selectOrFailByKey(dbSession, projectKey).getTags()).isEmpty(); + } +} -- 2.39.5