diff options
author | Julien Lancelot <julien.lancelot@gmail.com> | 2013-06-03 11:16:23 +0200 |
---|---|---|
committer | Julien Lancelot <julien.lancelot@gmail.com> | 2013-06-03 11:16:23 +0200 |
commit | 507e8b9a0c214ca86722af265cb3fa81c3d0c09f (patch) | |
tree | 1e8e09d3197c8eda121a414ab57a4b9ba4118c40 | |
parent | 6ce3ec7f1b3f0b3c1baf823037873bdd672af4e5 (diff) | |
download | sonarqube-507e8b9a0c214ca86722af265cb3fa81c3d0c09f.tar.gz sonarqube-507e8b9a0c214ca86722af265cb3fa81c3d0c09f.zip |
SONAR-4315 Add Issue Action WS and add Action links to Issue detail
19 files changed, 303 insertions, 348 deletions
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Action.java b/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Action.java index 113b1ea7fe3..6da2f1ecbc4 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Action.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Action.java @@ -22,14 +22,14 @@ package org.sonar.api.issue.action; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.Lists; -import org.sonar.api.ServerComponent; +import org.sonar.api.ServerExtension; import org.sonar.api.issue.Issue; import org.sonar.api.issue.condition.Condition; import java.util.Arrays; import java.util.List; -public class Action implements ServerComponent { +public class Action implements ServerExtension { private final String key; private final List<Condition> conditions; diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Actions.java b/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Actions.java deleted file mode 100644 index d883ab8a5c7..00000000000 --- a/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Actions.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2013 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube 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. - * - * SonarQube 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.api.issue.action; - -import org.sonar.api.ServerComponent; - -import java.util.Set; - -/** - * @since 3.6 - */ -// TODO remove it -public interface Actions extends ServerComponent { - - Actions addAction(Action action); - - Set<Action> getActions(); - -} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/workflow/function/CommentFunction.java b/sonar-plugin-api/src/main/java/org/sonar/api/workflow/function/CommentFunction.java deleted file mode 100644 index 697e9b5f2a0..00000000000 --- a/sonar-plugin-api/src/main/java/org/sonar/api/workflow/function/CommentFunction.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2013 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube 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. - * - * SonarQube 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.api.workflow.function; - -import com.google.common.annotations.Beta; -import org.sonar.api.issue.action.Function; - -/** - * @since 3.1 - */ -@Beta -public final class CommentFunction implements Function { - - @Override - public void execute(Context context) { - context.addComment("New comment!"); - } -} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/workflow/function/package-info.java b/sonar-plugin-api/src/main/java/org/sonar/api/workflow/function/package-info.java deleted file mode 100644 index 3285b8fdd91..00000000000 --- a/sonar-plugin-api/src/main/java/org/sonar/api/workflow/function/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2013 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube 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. - * - * SonarQube 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.api.workflow.function; - -import javax.annotation.ParametersAreNonnullByDefault;
\ No newline at end of file diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/workflow/function/CommentFunctionTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/workflow/function/CommentFunctionTest.java deleted file mode 100644 index 1d0b9ba3da6..00000000000 --- a/sonar-plugin-api/src/test/java/org/sonar/api/workflow/function/CommentFunctionTest.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2013 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube 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. - * - * SonarQube 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.api.workflow.function; - -import org.junit.Test; - -public class CommentFunctionTest { - - @Test - // TODO - public void setTextAndUserId() { -// CommentFunction function = new CommentFunction(); -// Map<String, String> parameters = Maps.newHashMap(); -// parameters.put("text", "foo"); -// DefaultReview review = new DefaultReview(); -// DefaultWorkflowContext context = new DefaultWorkflowContext(); -// context.setUserId(1234L); -// -// function.doExecute(review, new DefaultReview(), context, parameters); -// -// List<Comment> newComments = review.getNewComments(); -// assertThat(newComments).hasSize(1); -// assertThat(newComments.get(0).getMarkdownText()).isEqualTo("foo"); -// assertThat(newComments.get(0).getUserId()).isEqualTo(1234L); - } -} diff --git a/sonar-server/src/main/java/org/sonar/server/issue/ActionService.java b/sonar-server/src/main/java/org/sonar/server/issue/ActionService.java index 41b9a79a12e..301d0d2bf15 100644 --- a/sonar-server/src/main/java/org/sonar/server/issue/ActionService.java +++ b/sonar-server/src/main/java/org/sonar/server/issue/ActionService.java @@ -40,9 +40,9 @@ import org.sonar.server.user.UserSession; import javax.annotation.CheckForNull; import javax.annotation.Nullable; +import java.util.Collections; import java.util.Date; import java.util.List; -import java.util.Map; import static com.google.common.collect.Lists.newArrayList; @@ -52,7 +52,6 @@ public class ActionService implements ServerComponent { private final IssueStorage issueStorage; private final IssueUpdater updater; private final List<Action> actions; -// private final DefaultActions actions; public ActionService(DefaultIssueFinder finder, IssueStorage issueStorage, IssueUpdater updater, List<Action> actions) { this.finder = finder; @@ -61,10 +60,11 @@ public class ActionService implements ServerComponent { this.actions = actions; } - public List<Action> listAvailableActions(String issueKey) { - IssueQueryResult queryResult = loadIssue(issueKey); - final DefaultIssue issue = (DefaultIssue) queryResult.first(); + public ActionService(DefaultIssueFinder finder, IssueStorage issueStorage, IssueUpdater updater) { + this(finder, issueStorage, updater, Collections.<Action>emptyList()); + } + public List<Action> listAvailableActions(final Issue issue) { return newArrayList(Iterables.filter(actions, new Predicate<Action>() { @Override public boolean apply(Action action) { @@ -73,32 +73,31 @@ public class ActionService implements ServerComponent { })); } - @CheckForNull - public Action getAction(final String actionKey) { - return Iterables.find(actions, new Predicate<Action>() { - @Override - public boolean apply(Action action) { - return action.key().equals(actionKey); - } - }, null); + public List<Action> listAvailableActions(String issueKey) { + IssueQueryResult queryResult = loadIssue(issueKey); + final DefaultIssue issue = (DefaultIssue) queryResult.first(); + if (issue == null) { + throw new IllegalArgumentException("Issue is not found : " + issueKey); + } + + return listAvailableActions(issue); } - public Issue execute(String issueKey, String actionKey, UserSession userSession, Map<String, String> parameters) { public Issue execute(String issueKey, String actionKey, UserSession userSession) { Preconditions.checkArgument(!Strings.isNullOrEmpty(actionKey), "Missing action"); IssueQueryResult queryResult = loadIssue(issueKey); DefaultIssue issue = (DefaultIssue) queryResult.first(); if (issue == null) { - throw new IllegalStateException("Issue is not found : " + issueKey); + throw new IllegalArgumentException("Issue is not found : " + issueKey); } Action action = getAction(actionKey); if (action == null) { - throw new IllegalStateException("Action is not found : " + actionKey); + throw new IllegalArgumentException("Action is not found : " + actionKey); } if (!action.supports(issue)) { - throw new IllegalStateException("A condition is not respected."); + throw new IllegalStateException("A condition is not respected"); } IssueChangeContext changeContext = IssueChangeContext.createUser(new Date(), userSession.login()); @@ -115,6 +114,16 @@ public class ActionService implements ServerComponent { return finder.find(query); } + @CheckForNull + private Action getAction(final String actionKey) { + return Iterables.find(actions, new Predicate<Action>() { + @Override + public boolean apply(Action action) { + return action.key().equals(actionKey); + } + }, null); + } + static class FunctionContext implements Function.Context { private final DefaultIssue issue; @@ -133,11 +142,6 @@ public class ActionService implements ServerComponent { } @Override - public Map<String, String> parameters() { - return parameters; - } - - @Override public Function.Context setAttribute(String key, @Nullable String value) { updater.setAttribute(issue, key, value, changeContext); return this; diff --git a/sonar-server/src/main/java/org/sonar/server/issue/DefaultActions.java b/sonar-server/src/main/java/org/sonar/server/issue/DefaultActions.java deleted file mode 100644 index 7e80303bb7a..00000000000 --- a/sonar-server/src/main/java/org/sonar/server/issue/DefaultActions.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2013 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube 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. - * - * SonarQube 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.issue; - -import com.google.common.base.Predicate; -import com.google.common.collect.Iterables; -import com.google.common.collect.Sets; -import org.sonar.api.issue.action.Action; -import org.sonar.api.issue.action.Actions; - -import javax.annotation.CheckForNull; - -import java.util.Set; - -/** - * @since 3.6 - */ -public final class DefaultActions implements Actions { - - private Set<Action> actions = Sets.newLinkedHashSet(); - - public DefaultActions addAction(Action action) { - actions.add(action); - return this; - } - - public Set<Action> getActions() { - return actions; - } - - @CheckForNull - public Action getAction(final String actionKey) { - return Iterables.find(actions, new Predicate<Action>() { - @Override - public boolean apply(Action action) { - return action.key().equals(actionKey); - } - }, null); - } - -} diff --git a/sonar-server/src/main/java/org/sonar/server/issue/ExtendWorkflow.java b/sonar-server/src/main/java/org/sonar/server/issue/ExtendWorkflow.java deleted file mode 100644 index 2cdc076b4d6..00000000000 --- a/sonar-server/src/main/java/org/sonar/server/issue/ExtendWorkflow.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2013 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube 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. - * - * SonarQube 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.issue; - -import org.sonar.api.ServerExtension; -import org.sonar.api.issue.Issue; -import org.sonar.api.issue.action.Action; -import org.sonar.api.issue.action.Actions; -import org.sonar.api.issue.condition.HasResolution; -import org.sonar.api.workflow.function.CommentFunction; - -public final class ExtendWorkflow implements ServerExtension { - - private final Actions actions; - - public ExtendWorkflow(Actions actions) { - this.actions = actions; - } - - public void start() { - actions.addAction(Action.builder("fake") - .conditions(new HasResolution(Issue.RESOLUTION_FIXED)) - .functions(new CommentFunction()) - .build() - ); - } -} - diff --git a/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java b/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java index 4944d000b08..adb53c4b0c6 100644 --- a/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java +++ b/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java @@ -45,6 +45,7 @@ import org.sonar.server.user.UserSession; import org.sonar.server.util.RubyUtils; import javax.annotation.Nullable; + import java.util.Collection; import java.util.Date; import java.util.List; @@ -318,4 +319,8 @@ public class InternalRubyIssueService implements ServerComponent { public List<Action> listActions(String issueKey){ return actionService.listAvailableActions(issueKey); } + + public List<Action> listActions(Issue issue) { + return actionService.listAvailableActions(issue); + } }
\ No newline at end of file diff --git a/sonar-server/src/main/java/org/sonar/server/platform/Platform.java b/sonar-server/src/main/java/org/sonar/server/platform/Platform.java index 57972aa6eb0..f17401cce00 100644 --- a/sonar-server/src/main/java/org/sonar/server/platform/Platform.java +++ b/sonar-server/src/main/java/org/sonar/server/platform/Platform.java @@ -22,9 +22,6 @@ package org.sonar.server.platform; import org.apache.commons.configuration.BaseConfiguration; import org.slf4j.LoggerFactory; import org.sonar.api.config.EmailSettings; -import org.sonar.api.issue.Issue; -import org.sonar.api.issue.action.Action; -import org.sonar.api.issue.condition.HasResolution; import org.sonar.api.platform.ComponentContainer; import org.sonar.api.platform.Server; import org.sonar.api.profiles.AnnotationProfileParser; @@ -37,7 +34,6 @@ import org.sonar.api.rules.XMLRuleParser; import org.sonar.api.utils.HttpDownloader; import org.sonar.api.utils.TimeProfiler; import org.sonar.api.utils.UriReader; -import org.sonar.api.workflow.function.CommentFunction; import org.sonar.core.component.SnapshotPerspectives; import org.sonar.core.config.Logback; import org.sonar.core.i18n.GwtI18n; @@ -211,7 +207,7 @@ public final class Platform { */ private void startServiceComponents() { servicesContainer = coreContainer.createChild(); - servicesContainer.addSingleton(DefaultActions.class); + servicesContainer.addSingleton(HttpDownloader.class); servicesContainer.addSingleton(UriReader.class); servicesContainer.addSingleton(UpdateCenterClient.class); @@ -275,13 +271,6 @@ public final class Platform { servicesContainer.addSingleton(IssueNotifications.class); servicesContainer.addSingleton(ActionService.class); - // TODO only for test - servicesContainer.addSingleton(Action.builder("fake") - .conditions(new HasResolution(Issue.RESOLUTION_FIXED)) - .functions(new CommentFunction()) - .build() - ); - // rules servicesContainer.addSingleton(RubyRuleService.class); diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/issue_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/issue_controller.rb index fe6e05e0397..9f1fb5c4f51 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/issue_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/issue_controller.rb @@ -81,6 +81,9 @@ class IssueController < ApplicationController Internal.issues.plan(issue_key, params[:plan]) elsif action_type=='unplan' Internal.issues.plan(issue_key, nil) + else + # Execute action defined by plugin + Internal.issues.executeAction(issue_key, action_type) end @issue_results = Api.issues.find(issue_key) diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/issue/_issue.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/issue/_issue.html.erb index de7d6ba2812..7ea645906f3 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/issue/_issue.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/issue/_issue.html.erb @@ -92,7 +92,9 @@ <% transitions = Internal.issues.listTransitions(issue) - if transitions.size > 0 && transitions.first + + # Display only the first transition + if !transitions.empty? && transitions.first transition = transitions.first %> <%= image_tag 'sep12.png' -%> @@ -101,7 +103,10 @@ <% end %> <% - if transitions.size > 1 || !issue.resolution + plugin_actions = Internal.issues.listActions(issue) + puts "### " + shouldDisplayDropDown = transitions.size > 1 || !issue.resolution || !plugin_actions.empty? + if shouldDisplayDropDown transitions.remove(0) %> <div class="dropdown"> @@ -112,11 +117,22 @@ <a href="#" onclick="return issueForm('severity', this)" class="link-action spacer-right"><%= message("issue.set_severity") -%></a> </li> <% end %> - <% transitions.each do |transition| %> + + <% + # Display remaining transitions + transitions.each do |transition| %> <li> <a href="#" onclick="return doIssueTransition(this, '<%= transition.key -%>')" class="link-action spacer-right"><%= message("issue.transition.#{transition.key}") -%></a> </li> <% end %> + + <% + # Display actions defined by plugins + plugin_actions.each do |action| %> + <li> + <a href="#" onclick="return doPluginIssueAction(this, '<%= action.key -%>')" class="link-action spacer-right"><%= message("issue.action.#{action.key}.formlink") -%></a> + </li> + <% end %> </ul> </div> <% end %> diff --git a/sonar-server/src/main/webapp/javascripts/issue.js b/sonar-server/src/main/webapp/javascripts/issue.js index e034916d251..ca2e2818e70 100644 --- a/sonar-server/src/main/webapp/javascripts/issue.js +++ b/sonar-server/src/main/webapp/javascripts/issue.js @@ -93,6 +93,12 @@ function doIssueAction(elt, action, parameters) { return false; } +// Used for actions defined by plugins +function doPluginIssueAction(elt, action) { + var parameters = {}; + return doIssueAction(elt, action, parameters) +} + function assignIssueToMe(elt) { var parameters = {'me': true}; return doIssueAction(elt, 'assign', parameters) diff --git a/sonar-server/src/test/java/org/sonar/server/issue/ActionServiceTest.java b/sonar-server/src/test/java/org/sonar/server/issue/ActionServiceTest.java index c5a796232fa..7ef368d9f95 100644 --- a/sonar-server/src/test/java/org/sonar/server/issue/ActionServiceTest.java +++ b/sonar-server/src/test/java/org/sonar/server/issue/ActionServiceTest.java @@ -20,23 +20,192 @@ package org.sonar.server.issue; -import org.junit.Before; +import org.junit.Test; +import org.sonar.api.issue.Issue; +import org.sonar.api.issue.IssueQuery; +import org.sonar.api.issue.IssueQueryResult; +import org.sonar.api.issue.action.Action; +import org.sonar.api.issue.action.Function; +import org.sonar.api.issue.condition.Condition; +import org.sonar.api.issue.internal.DefaultIssue; +import org.sonar.api.issue.internal.IssueChangeContext; +import org.sonar.core.issue.DefaultIssueQueryResult; import org.sonar.core.issue.IssueUpdater; import org.sonar.core.issue.db.IssueStorage; +import org.sonar.server.user.UserSession; -import static org.mockito.Mockito.mock; +import java.util.Collections; + +import static com.google.common.collect.Lists.newArrayList; +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.*; public class ActionServiceTest { private ActionService actionService; - private DefaultIssueFinder finder = mock(DefaultIssueFinder.class); private IssueStorage issueStorage = mock(IssueStorage.class); private IssueUpdater updater = mock(IssueUpdater.class); - private DefaultActions actions = mock(DefaultActions.class); - @Before - public void before(){ + @Test + public void should_execute_functions() { + Function function1 = mock(Function.class); + Function function2 = mock(Function.class); + + Issue issue = new DefaultIssue().setKey("ABCD"); + IssueQueryResult issueQueryResult = new DefaultIssueQueryResult(newArrayList(issue)); + when(finder.find(any(IssueQuery.class))).thenReturn(issueQueryResult); + + Action action = Action.builder("link-to-jira").conditions(new AlwaysMatch()).functions(function1, function2).build(); + + actionService = new ActionService(finder, issueStorage, updater, newArrayList(action)); + assertThat(actionService.execute("ABCD", "link-to-jira", mock(UserSession.class))).isNotNull(); + + verify(function1).execute(any(Function.Context.class)); + verify(function2).execute(any(Function.Context.class)); + verifyNoMoreInteractions(function1, function2); + } + + @Test + public void should_modify_issue_when_executing_a_function() { + Function function = new TweetFunction(); + + UserSession userSession = mock(UserSession.class); + when(userSession.login()).thenReturn("arthur"); + + DefaultIssue issue = new DefaultIssue().setKey("ABCD"); + IssueQueryResult issueQueryResult = new DefaultIssueQueryResult(newArrayList((Issue) issue)); + when(finder.find(any(IssueQuery.class))).thenReturn(issueQueryResult); + + Action action = Action.builder("link-to-jira").conditions(new AlwaysMatch()).functions(function).build(); + + actionService = new ActionService(finder, issueStorage, updater, newArrayList(action)); + assertThat(actionService.execute("ABCD", "link-to-jira", userSession)).isNotNull(); + + verify(updater).addComment(eq(issue), eq("New tweet on issue ABCD"), any(IssueChangeContext.class)); + verify(updater).setAttribute(eq(issue), eq("tweet"), eq("tweet sent"), any(IssueChangeContext.class)); + } + + @Test + public void should_not_execute_function_if_issue_not_found() { + Function function = mock(Function.class); + + IssueQueryResult issueQueryResult = new DefaultIssueQueryResult(Collections.<Issue>emptyList()); + when(finder.find(any(IssueQuery.class))).thenReturn(issueQueryResult); + + Action action = Action.builder("link-to-jira").conditions(new AlwaysMatch()).functions(function).build(); + + actionService = new ActionService(finder, issueStorage, updater, newArrayList(action)); + + try { + actionService.execute("ABCD", "link-to-jira", mock(UserSession.class)); + } catch (Exception e){ + assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Issue is not found : ABCD"); + } + verifyZeroInteractions(function); + } + + @Test + public void should_not_execute_function_if_action_not_found() { + Function function = mock(Function.class); + + Issue issue = new DefaultIssue().setKey("ABCD"); + IssueQueryResult issueQueryResult = new DefaultIssueQueryResult(newArrayList(issue)); + when(finder.find(any(IssueQuery.class))).thenReturn(issueQueryResult); + + Action action = Action.builder("link-to-jira").conditions(new AlwaysMatch()).functions(function).build(); + + actionService = new ActionService(finder, issueStorage, updater, newArrayList(action)); + try { + actionService.execute("ABCD", "tweet", mock(UserSession.class)); + } catch (Exception e){ + assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Action is not found : tweet"); + } + verifyZeroInteractions(function); + } + + @Test + public void should_not_execute_function_if_action_is_not_supported() { + Function function = mock(Function.class); + + Issue issue = new DefaultIssue().setKey("ABCD"); + IssueQueryResult issueQueryResult = new DefaultIssueQueryResult(newArrayList(issue)); + when(finder.find(any(IssueQuery.class))).thenReturn(issueQueryResult); + + Action action = Action.builder("link-to-jira").conditions(new NeverMatch()).functions(function).build(); + + actionService = new ActionService(finder, issueStorage, updater, newArrayList(action)); + try { + actionService.execute("ABCD", "link-to-jira", mock(UserSession.class)); + } catch (Exception e){ + assertThat(e).isInstanceOf(IllegalStateException.class).hasMessage("A condition is not respected"); + } + verifyZeroInteractions(function); + } + + @Test + public void should_list_available_supported_actions() { + Issue issue = new DefaultIssue().setKey("ABCD"); + IssueQueryResult issueQueryResult = new DefaultIssueQueryResult(newArrayList(issue)); + when(finder.find(any(IssueQuery.class))).thenReturn(issueQueryResult); + + Action action1 = Action.builder("link-to-jira").conditions(new AlwaysMatch()).build(); + Action action2 = Action.builder("tweet").conditions(new NeverMatch()).build(); + + actionService = new ActionService(finder, issueStorage, updater, newArrayList(action1, action2)); + assertThat(actionService.listAvailableActions("ABCD")).containsOnly(action1); + } + + @Test + public void should_list_available_actions_throw_exception_if_issue_not_found() { + IssueQueryResult issueQueryResult = new DefaultIssueQueryResult(Collections.<Issue>emptyList()); + when(finder.find(any(IssueQuery.class))).thenReturn(issueQueryResult); + + Action action1 = Action.builder("link-to-jira").conditions(new AlwaysMatch()).build(); + Action action2 = Action.builder("tweet").conditions(new NeverMatch()).build(); + + actionService = new ActionService(finder, issueStorage, updater, newArrayList(action1, action2)); + + try { + actionService.listAvailableActions("ABCD"); + fail(); + } catch (Exception e){ + assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Issue is not found : ABCD"); + } + } + + @Test + public void should_return_no_action() { + Issue issue = new DefaultIssue().setKey("ABCD"); + IssueQueryResult issueQueryResult = new DefaultIssueQueryResult(newArrayList(issue)); + when(finder.find(any(IssueQuery.class))).thenReturn(issueQueryResult); + + actionService = new ActionService(finder, issueStorage, updater); + assertThat(actionService.listAvailableActions("ABCD")).isEmpty(); + } + + public class AlwaysMatch implements Condition { + @Override + public boolean matches(Issue issue) { + return true; + } + } + + public class NeverMatch implements Condition { + @Override + public boolean matches(Issue issue) { + return false; + } + } + public class TweetFunction implements Function { + @Override + public void execute(Context context) { + context.addComment("New tweet on issue "+ context.issue().key()); + context.setAttribute("tweet", "tweet sent"); + } } } diff --git a/sonar-server/src/test/java/org/sonar/server/issue/DefaultActionsTest.java b/sonar-server/src/test/java/org/sonar/server/issue/DefaultActionsTest.java deleted file mode 100644 index 08622ff2677..00000000000 --- a/sonar-server/src/test/java/org/sonar/server/issue/DefaultActionsTest.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2013 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube 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. - * - * SonarQube 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.issue; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.sonar.api.issue.action.Action; - -import static org.fest.assertions.Assertions.assertThat; - -public class DefaultActionsTest { - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - private DefaultActions actions; - - @Before - public void before(){ - actions = new DefaultActions(); - } - - @Test - public void should_add_action() { - Action action = Action.builder("link-to-jira").build(); - - actions.addAction(action); - - assertThat(actions.getActions()).hasSize(1); - } - - @Test - public void should_get_action() { - Action action = Action.builder("link-to-jira").build(); - - actions.addAction(action); - - assertThat(actions.getAction("link-to-jira")).isEqualTo(action); - assertThat(actions.getAction("not-found")).isNull(); - } - -} diff --git a/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/DefaultIssueClient.java b/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/DefaultIssueClient.java index 727a2e4c409..4dd1972e2ba 100644 --- a/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/DefaultIssueClient.java +++ b/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/DefaultIssueClient.java @@ -109,7 +109,7 @@ public class DefaultIssueClient implements IssueClient { Map<String, Object> queryParams = EncodingUtils.toMap("issue", issueKey); HttpRequest request = requestFactory.get("/api/issues/transitions", queryParams); if (!request.ok()) { - throw new IllegalStateException("Fail to return transition for issue. Bad HTTP response status: " + request.code()); + throw new IllegalStateException("Fail to return transitions for issue. Bad HTTP response status: " + request.code()); } String json = request.body("UTF-8"); return parser.parseTransitions(json); @@ -125,6 +125,27 @@ public class DefaultIssueClient implements IssueClient { return createIssueResult(request); } + @Override + public List<String> actions(String issueKey) { + Map<String, Object> queryParams = EncodingUtils.toMap("issue", issueKey); + HttpRequest request = requestFactory.get("/api/issues/actions", queryParams); + if (!request.ok()) { + throw new IllegalStateException("Fail to return actions for issue. Bad HTTP response status: " + request.code()); + } + String json = request.body("UTF-8"); + return parser.parseActions(json); + } + + @Override + public Issue doAction(String issueKey, String action) { + Map<String, Object> params = EncodingUtils.toMap("issue", issueKey, "actionKey", action); + HttpRequest request = requestFactory.post("/api/issues/do_action", params); + if (!request.ok()) { + throw new IllegalStateException("Fail to execute action on issue " + issueKey + ".Bad HTTP response status: " + request.code()); + } + return createIssueResult(request); + } + private Issue createIssueResult(HttpRequest request){ String json = request.body("UTF-8"); Map jsonRoot = (Map) JSONValue.parse(json); diff --git a/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueClient.java b/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueClient.java index 2130d3c92ae..3527938710a 100644 --- a/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueClient.java +++ b/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueClient.java @@ -44,4 +44,8 @@ public interface IssueClient { Issue doTransition(String issueKey, String transition); + List<String> actions(String issueKey); + + Issue doAction(String issueKey, String action); + } diff --git a/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueJsonParser.java b/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueJsonParser.java index 635a624758b..906929ce308 100644 --- a/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueJsonParser.java +++ b/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueJsonParser.java @@ -112,4 +112,14 @@ class IssueJsonParser { } return transitions; } + + List<String> parseActions(String json) { + List<String> actions = new ArrayList<String>(); + Map jRoot = (Map) JSONValue.parse(json); + List<String> jActions = (List) jRoot.get("actions"); + for (String jAction : jActions) { + actions.add(jAction); + } + return actions; + } } diff --git a/sonar-ws-client/src/test/java/org/sonar/wsclient/issue/DefaultIssueClientTest.java b/sonar-ws-client/src/test/java/org/sonar/wsclient/issue/DefaultIssueClientTest.java index 41118a61ff9..7ad6f6df3c8 100644 --- a/sonar-ws-client/src/test/java/org/sonar/wsclient/issue/DefaultIssueClientTest.java +++ b/sonar-ws-client/src/test/java/org/sonar/wsclient/issue/DefaultIssueClientTest.java @@ -179,4 +179,34 @@ public class DefaultIssueClientTest { assertThat(comment.login()).isEqualTo("admin"); assertThat(comment.createdAt().getDate()).isEqualTo(18); } + + @Test + public void should_get_actions() { + HttpRequestFactory requestFactory = new HttpRequestFactory(httpServer.url()); + httpServer.doReturnBody("{\n" + + " \"actions\": [\n" + + " \"link-to-jira\",\n" + + " \"tweet\"\n" + + " ]\n" + + "}"); + + IssueClient client = new DefaultIssueClient(requestFactory); + List<String> actions = client.actions("ABCDE"); + + assertThat(httpServer.requestedPath()).isEqualTo("/api/issues/actions?issue=ABCDE"); + assertThat(actions).hasSize(2); + assertThat(actions).containsOnly("link-to-jira", "tweet"); + } + + @Test + public void should_apply_action() { + HttpRequestFactory requestFactory = new HttpRequestFactory(httpServer.url()); + httpServer.doReturnBody("{\"issue\": {\"key\": \"ABCDE\"}}"); + + IssueClient client = new DefaultIssueClient(requestFactory); + Issue result = client.doAction("ABCDE", "tweet"); + + assertThat(httpServer.requestedPath()).isEqualTo("/api/issues/do_action?issue=ABCDE&actionKey=tweet"); + assertThat(result).isNotNull(); + } } |