]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6717 Restore issues actions API
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 7 Oct 2015 12:12:10 +0000 (14:12 +0200)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Fri, 9 Oct 2015 14:48:24 +0000 (16:48 +0200)
30 files changed:
it/it-plugins/issue-action-plugin/pom.xml [new file with mode: 0644]
it/it-plugins/issue-action-plugin/src/main/java/ActionDefinition.java [new file with mode: 0644]
it/it-plugins/issue-action-plugin/src/main/java/IssueActionPlugin.java [new file with mode: 0644]
it/it-plugins/issue-action-plugin/src/main/resources/org/sonar/l10n/issueaction.properties [new file with mode: 0644]
it/it-plugins/pom.xml
it/it-tests/src/test/java/issue/suite/IssueActionTest.java [new file with mode: 0644]
it/it-tests/src/test/java/issue/suite/IssueTestSuite.java
it/it-tests/src/test/java/util/ItUtils.java
it/it-tests/src/test/resources/issue/suite/IssueActionTest/xoo-one-issue-per-line-profile.xml [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/issue/ActionService.java
server/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueActionsWriter.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssuesWs.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseLoader.java
server/sonar-server/src/main/java/org/sonar/server/properties/ProjectSettings.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/properties/ProjectSettingsFactory.java
server/sonar-server/src/test/java/org/sonar/server/issue/ActionServiceTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueActionsWriterTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/ShowActionTest.java
server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsFactoryTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsRespositoryFactoryTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsRespositoryTest.java [deleted file]
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/ShowActionTest/show_issue_with_comments.json
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/ShowActionTest/show_issue_with_transitions.json
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/issues_controller.rb
sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Action.java
sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Actions.java

diff --git a/it/it-plugins/issue-action-plugin/pom.xml b/it/it-plugins/issue-action-plugin/pom.xml
new file mode 100644 (file)
index 0000000..6b176b8
--- /dev/null
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.sonarsource.it</groupId>
+    <artifactId>it-plugins</artifactId>
+    <version>5.2-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>issue-action-plugin</artifactId>
+  <packaging>sonar-plugin</packaging>
+  <name>Plugins :: Issue Action</name>
+  <version>1.0-SNAPSHOT</version>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.codehaus.sonar</groupId>
+      <artifactId>sonar-plugin-api</artifactId>
+      <version>${apiVersion}</version>
+      <scope>provided</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.codehaus.sonar</groupId>
+        <artifactId>sonar-packaging-maven-plugin</artifactId>
+        <version>1.1</version>
+        <extensions>true</extensions>
+        <configuration>
+          <pluginClass>IssueActionPlugin</pluginClass>
+          <pluginKey>issueaction</pluginKey>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/it/it-plugins/issue-action-plugin/src/main/java/ActionDefinition.java b/it/it-plugins/issue-action-plugin/src/main/java/ActionDefinition.java
new file mode 100644 (file)
index 0000000..84f5e6c
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.
+ */
+
+import org.sonar.api.ServerExtension;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.action.Actions;
+import org.sonar.api.issue.action.Function;
+import org.sonar.api.issue.condition.HasResolution;
+
+public class ActionDefinition implements ServerExtension {
+
+  private final Actions actions;
+
+  public ActionDefinition(Actions actions) {
+    this.actions = actions;
+  }
+
+  public void start() {
+    actions.add("fake")
+      .setConditions(new HasResolution(Issue.RESOLUTION_FIXED))
+      .setFunctions(new Function() {
+        @Override
+        public void execute(Context context) {
+          context.setAttribute("fake", "fake action");
+          context.addComment("New Comment from fake action");
+        }
+      });
+  }
+}
diff --git a/it/it-plugins/issue-action-plugin/src/main/java/IssueActionPlugin.java b/it/it-plugins/issue-action-plugin/src/main/java/IssueActionPlugin.java
new file mode 100644 (file)
index 0000000..d322d9f
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.
+ */
+
+import org.sonar.api.SonarPlugin;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class IssueActionPlugin extends SonarPlugin {
+
+  public List getExtensions() {
+    return Arrays.asList(
+      ActionDefinition.class
+    );
+  }
+
+}
diff --git a/it/it-plugins/issue-action-plugin/src/main/resources/org/sonar/l10n/issueaction.properties b/it/it-plugins/issue-action-plugin/src/main/resources/org/sonar/l10n/issueaction.properties
new file mode 100644 (file)
index 0000000..f507cee
--- /dev/null
@@ -0,0 +1,21 @@
+#
+# SonarQube, open source software quality management tool.
+# Copyright (C) 2008-2014 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.
+#
+
+issue.action.fake.formlink=Fake
index b0d7c374cef1671fc06f7f6673638995bd4d5416..984e022859ddebc6723eb8a911945c91de0b5c92 100644 (file)
@@ -35,6 +35,7 @@
     <module>batch-plugin</module>
     <module>extension-lifecycle-plugin</module>
     <module>global-property-change-plugin</module>
+    <module>issue-action-plugin</module>
     <module>l10n-fr-pack</module>
     <module>license-plugin</module>
     <module>project-builder-plugin</module>
diff --git a/it/it-tests/src/test/java/issue/suite/IssueActionTest.java b/it/it-tests/src/test/java/issue/suite/IssueActionTest.java
new file mode 100644 (file)
index 0000000..ab23ce5
--- /dev/null
@@ -0,0 +1,257 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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 issue.suite;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarRunner;
+import com.sonar.orchestrator.locator.FileLocation;
+import java.util.List;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.sonar.wsclient.base.HttpException;
+import org.sonar.wsclient.issue.ActionPlan;
+import org.sonar.wsclient.issue.ActionPlanClient;
+import org.sonar.wsclient.issue.Issue;
+import org.sonar.wsclient.issue.IssueComment;
+import org.sonar.wsclient.issue.IssueQuery;
+import org.sonar.wsclient.issue.Issues;
+import org.sonar.wsclient.issue.NewActionPlan;
+
+import static issue.suite.IssueTestSuite.ORCHESTRATOR;
+import static issue.suite.IssueTestSuite.adminIssueClient;
+import static issue.suite.IssueTestSuite.issueClient;
+import static issue.suite.IssueTestSuite.search;
+import static issue.suite.IssueTestSuite.searchIssueByKey;
+import static issue.suite.IssueTestSuite.searchIssues;
+import static issue.suite.IssueTestSuite.searchRandomIssue;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static util.ItUtils.projectDir;
+import static util.ItUtils.toDate;
+import static util.ItUtils.verifyHttpException;
+
+public class IssueActionTest {
+
+  @ClassRule
+  public static Orchestrator orchestrator = ORCHESTRATOR;
+
+  Issue issue;
+  SonarRunner scan;
+
+  private static List<Issue> searchIssuesBySeverities(String componentKey, String... severities) {
+    return searchIssues(IssueQuery.create().componentRoots(componentKey).severities(severities));
+  }
+
+  private static ActionPlanClient adminActionPlanClient() {
+    return orchestrator.getServer().adminWsClient().actionPlanClient();
+  }
+
+  @Before
+  public void resetData() {
+    orchestrator.resetData();
+    orchestrator.getServer().restoreProfile(FileLocation.ofClasspath("/issue/suite/IssueActionTest/xoo-one-issue-per-line-profile.xml"));
+    orchestrator.getServer().provisionProject("sample", "Sample");
+    orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "xoo-one-issue-per-line-profile");
+
+    scan = SonarRunner.create(projectDir("shared/xoo-sample"));
+    orchestrator.executeBuild(scan);
+    issue = searchRandomIssue();
+  }
+
+  @Test
+  public void no_comments_by_default() throws Exception {
+    assertThat(issue.comments()).isEmpty();
+  }
+
+  @Test
+  public void add_comment() throws Exception {
+    IssueComment comment = adminIssueClient().addComment(issue.key(), "this is my *comment*");
+    assertThat(comment.key()).isNotNull();
+    assertThat(comment.htmlText()).isEqualTo("this is my <em>comment</em>");
+    assertThat(comment.login()).isEqualTo("admin");
+    assertThat(comment.createdAt()).isNotNull();
+
+    // reload issue
+    Issue reloaded = searchIssueByKey(issue.key());
+
+    assertThat(reloaded.comments()).hasSize(1);
+    assertThat(reloaded.comments().get(0).key()).isEqualTo(comment.key());
+    assertThat(reloaded.comments().get(0).htmlText()).isEqualTo("this is my <em>comment</em>");
+    assertThat(reloaded.updateDate().before(issue.creationDate())).isFalse();
+  }
+
+  /**
+   * SONAR-4450
+   */
+  @Test
+  public void should_reject_blank_comment() throws Exception {
+    try {
+      adminIssueClient().addComment(issue.key(), "  ");
+      fail();
+    } catch (HttpException ex) {
+      assertThat(ex.status()).isEqualTo(400);
+    }
+
+    Issue reloaded = searchIssueByKey(issue.key());
+    assertThat(reloaded.comments()).hasSize(0);
+  }
+
+  /**
+   * SONAR-4352
+   */
+  @Test
+  public void change_severity() {
+    String componentKey = "sample";
+
+    // there are no blocker issues
+    assertThat(searchIssuesBySeverities(componentKey, "BLOCKER")).isEmpty();
+
+    // increase the severity of an issue
+    adminIssueClient().setSeverity(issue.key(), "BLOCKER");
+
+    assertThat(searchIssuesBySeverities(componentKey, "BLOCKER")).hasSize(1);
+
+    orchestrator.executeBuild(scan);
+    Issue reloaded = searchIssueByKey(issue.key());
+    assertThat(reloaded.severity()).isEqualTo("BLOCKER");
+    assertThat(reloaded.status()).isEqualTo("OPEN");
+    assertThat(reloaded.resolution()).isNull();
+    assertThat(reloaded.creationDate()).isEqualTo(issue.creationDate());
+    assertThat(reloaded.creationDate().before(reloaded.updateDate())).isTrue();
+  }
+
+  /**
+   * SONAR-4287
+   */
+  @Test
+  public void assign() {
+    assertThat(issue.assignee()).isNull();
+    Issues issues = search(IssueQuery.create().issues(issue.key()));
+    assertThat(issues.users()).isEmpty();
+
+    adminIssueClient().assign(issue.key(), "admin");
+    assertThat(search(IssueQuery.create().assignees("admin")).list()).hasSize(1);
+
+    orchestrator.executeBuild(scan);
+    Issue reloaded = searchIssueByKey(issue.key());
+    assertThat(reloaded.assignee()).isEqualTo("admin");
+    assertThat(reloaded.creationDate()).isEqualTo(issue.creationDate());
+
+    issues = search(IssueQuery.create().issues(issue.key()));
+    assertThat(issues.user("admin")).isNotNull();
+    assertThat(issues.user("admin").name()).isEqualTo("Administrator");
+
+    // unassign
+    adminIssueClient().assign(issue.key(), null);
+    reloaded = searchIssueByKey(issue.key());
+    assertThat(reloaded.assignee()).isNull();
+    assertThat(issueClient().find(IssueQuery.create().assignees("admin")).list()).isEmpty();
+  }
+
+  /**
+   * SONAR-4287
+   */
+  @Test
+  public void fail_assign_if_assignee_does_not_exist() {
+    assertThat(issue.assignee()).isNull();
+    try {
+      adminIssueClient().assign(issue.key(), "unknown");
+      fail();
+    } catch (Exception e) {
+      verifyHttpException(e, 400);
+    }
+  }
+
+  /**
+   * SONAR-4290
+   */
+  @Test
+  public void plan() {
+    assertThat(issue.actionPlan()).isNull();
+
+    // Set action plan to issue
+    ActionPlan newActionPlan = adminActionPlanClient().create(NewActionPlan.create().name("Short term").project("sample")
+      .description("Short term issues").deadLine(toDate("2113-01-31")));
+    assertThat(newActionPlan.key()).isNotNull();
+    adminIssueClient().plan(issue.key(), newActionPlan.key());
+    assertThat(search(IssueQuery.create().actionPlans(newActionPlan.key())).list()).hasSize(1);
+
+    orchestrator.executeBuild(scan);
+    Issue reloaded = searchIssueByKey(issue.key());
+    assertThat(reloaded.actionPlan()).isEqualTo(newActionPlan.key());
+    assertThat(reloaded.creationDate()).isEqualTo(issue.creationDate());
+    ActionPlan actionPlan = search(IssueQuery.create().actionPlans(newActionPlan.key())).actionPlans(reloaded);
+    assertThat(actionPlan.name()).isEqualTo(newActionPlan.name());
+    assertThat(actionPlan.deadLine()).isEqualTo(newActionPlan.deadLine());
+  }
+
+  @Test
+  public void fail_plan_if_action_plan_does_not_exist() {
+    assertThat(issue.actionPlan()).isNull();
+    try {
+      adminIssueClient().plan(issue.key(), "unknown");
+      fail();
+    } catch (Exception e) {
+      verifyHttpException(e, 400);
+    }
+  }
+
+  @Test
+  public void unplan() {
+    assertThat(issue.actionPlan()).isNull();
+
+    // Set action plan to issue
+    ActionPlan newActionPlan = adminActionPlanClient().create(NewActionPlan.create().name("Short term").project("sample")
+      .description("Short term issues").deadLine(toDate("2113-01-31")));
+    assertThat(newActionPlan.key()).isNotNull();
+    adminIssueClient().plan(issue.key(), newActionPlan.key());
+    assertThat(search(IssueQuery.create().actionPlans(newActionPlan.key())).list()).hasSize(1);
+
+    // Unplan
+    adminIssueClient().plan(issue.key(), null);
+    assertThat(search(IssueQuery.create().actionPlans(newActionPlan.key())).list()).hasSize(0);
+
+    orchestrator.executeBuild(scan);
+    Issue reloaded = searchIssueByKey(issue.key());
+    assertThat(reloaded.actionPlan()).isNull();
+    assertThat(reloaded.creationDate()).isEqualTo(issue.creationDate());
+  }
+
+  /**
+   * SONAR-4315
+   */
+  @Test
+  public void apply_action_from_plugin() {
+    // The condition on the action defined by the plugin is that the status must be resolved
+    adminIssueClient().doTransition(issue.key(), "resolve");
+    assertThat(adminIssueClient().actions(issue.key())).contains("fake");
+
+    adminIssueClient().doAction(issue.key(), "fake");
+
+    // reload issue
+    Issue reloaded = searchIssueByKey(issue.key());
+
+    assertThat(reloaded.comments()).hasSize(1);
+    assertThat(reloaded.comments().get(0).htmlText()).isEqualTo("New Comment from fake action");
+  }
+
+}
index 6757552276e03ed9ba03b2dbe0d0247cd6913f60..3e6a1e4ce027cf61e6fb23a6b6f9984c5274de3b 100644 (file)
@@ -13,42 +13,52 @@ import org.junit.runners.Suite;
 import org.sonar.wsclient.issue.Issue;
 import org.sonar.wsclient.issue.IssueClient;
 import org.sonar.wsclient.issue.IssueQuery;
-import util.ItUtils;
+import org.sonar.wsclient.issue.Issues;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.pluginArtifact;
+import static util.ItUtils.xooPlugin;
 
 @RunWith(Suite.class)
 @Suite.SuiteClasses({
-  CommonRulesTest.class, IssueWorkflowTest.class, ManualRulesTest.class, CustomRulesTest.class
+  CommonRulesTest.class, IssueWorkflowTest.class, ManualRulesTest.class, CustomRulesTest.class, IssueActionTest.class
 })
 public class IssueTestSuite {
 
   @ClassRule
   public static final Orchestrator ORCHESTRATOR = Orchestrator.builderEnv()
     .setSonarVersion("DEV")
-    .addPlugin(ItUtils.xooPlugin())
+    .addPlugin(xooPlugin())
+    .addPlugin(pluginArtifact("issue-action-plugin"))
     .build();
 
   static IssueClient adminIssueClient() {
     return ORCHESTRATOR.getServer().adminWsClient().issueClient();
   }
 
+  static IssueClient issueClient() {
+    return ORCHESTRATOR.getServer().wsClient().issueClient();
+  }
+
   static Issue searchRandomIssue() {
     List<Issue> issues = searchIssues(IssueQuery.create());
     assertThat(issues).isNotEmpty();
     return issues.get(0);
   }
 
-  static List<Issue> searchIssues(IssueQuery issueQuery) {
+  static Issues search(IssueQuery issueQuery) {
     issueQuery.urlParams().put("additionalFields", "_all");
-    return adminIssueClient().find(issueQuery).list();
+    return issueClient().find(issueQuery);
+  }
+
+  static List<Issue> searchIssues(IssueQuery issueQuery) {
+    return search(issueQuery).list();
   }
 
   static Issue searchIssueByKey(String issueKey) {
-    IssueQuery query = IssueQuery.create().issues(issueKey);
-    query.urlParams().put("additionalFields", "_all");
-    List<Issue> issues = searchIssues(query);
+    List<Issue> issues = searchIssues(IssueQuery.create().issues(issueKey));
     assertThat(issues).hasSize(1);
     return issues.get(0);
   }
+
 }
index 1d8e3406ea804b60d3583090a1ba1686ec0c84c3..4cb4df2c0455cd979e5b3288937901f92b1f7e9c 100644 (file)
@@ -9,6 +9,9 @@ import com.google.common.collect.Iterables;
 import com.sonar.orchestrator.Orchestrator;
 import com.sonar.orchestrator.build.BuildResult;
 import com.sonar.orchestrator.build.SonarRunner;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
 import org.json.simple.JSONArray;
 import org.json.simple.JSONObject;
 import org.json.simple.JSONValue;
@@ -27,6 +30,7 @@ import static org.assertj.core.api.Assertions.fail;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import org.apache.commons.io.FileUtils;
+import org.sonar.wsclient.base.HttpException;
 
 public class ItUtils {
 
@@ -157,4 +161,20 @@ public class ItUtils {
     }
     return from(Iterables.concat(asList(properties), asList(str))).toArray(String.class);
   }
+
+  public static void verifyHttpException(Exception e, int expectedCode) {
+    assertThat(e).isInstanceOf(HttpException.class);
+    HttpException exception = (HttpException) e;
+    assertThat(exception.status()).isEqualTo(expectedCode);
+  }
+
+  public static Date toDate(String sDate) {
+    try {
+      SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+      return sdf.parse(sDate);
+    } catch (ParseException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
 }
diff --git a/it/it-tests/src/test/resources/issue/suite/IssueActionTest/xoo-one-issue-per-line-profile.xml b/it/it-tests/src/test/resources/issue/suite/IssueActionTest/xoo-one-issue-per-line-profile.xml
new file mode 100644 (file)
index 0000000..608f80c
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0"?><!-- Generated by Sonar -->
+<profile>
+  <name>xoo-one-issue-per-line-profile</name>
+  <language>xoo</language>
+  <rules>
+    <rule>
+      <repositoryKey>xoo</repositoryKey>
+      <key>OneIssuePerLine</key>
+      <priority>CRITICAL</priority>
+    </rule>
+  </rules>
+</profile>
index 42da9fd1b8edcc9b708c2006b714408132db30cc..c57be962187213e0936ef51db3fb7aa303a01850 100644 (file)
 
 package org.sonar.server.issue;
 
+import com.google.common.base.Predicate;
+import com.google.common.base.Strings;
+import java.util.Date;
 import java.util.List;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.sonar.api.config.Settings;
+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.action.Function;
 import org.sonar.api.server.ServerSide;
-import org.sonar.api.web.UserRole;
-import org.sonar.db.issue.IssueDto;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.IssueChangeContext;
+import org.sonar.core.issue.IssueUpdater;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.server.properties.ProjectSettingsFactory;
 import org.sonar.server.user.UserSession;
 
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.FluentIterable.from;
+import static com.google.common.collect.Iterables.find;
 import static com.google.common.collect.Lists.newArrayList;
+import static org.sonar.api.web.UserRole.ISSUE_ADMIN;
 
 /**
  * @since 3.6
@@ -34,30 +53,163 @@ import static com.google.common.collect.Lists.newArrayList;
 @ServerSide
 public class ActionService {
 
+  private final DbClient dbClient;
   private final UserSession userSession;
+  private final Actions actions;
+  private final IssueService issueService;
+  private final IssueUpdater updater;
+  private final ProjectSettingsFactory projectSettingsFactory;
+  private final IssueStorage issueStorage;
 
-  public ActionService(UserSession userSession) {
+  public ActionService(DbClient dbClient, UserSession userSession, ProjectSettingsFactory projectSettingsFactory, Actions actions, IssueService issueService, IssueUpdater updater,
+    IssueStorage issueStorage) {
+    this.dbClient = dbClient;
     this.userSession = userSession;
+    this.actions = actions;
+    this.issueService = issueService;
+    this.updater = updater;
+    this.projectSettingsFactory = projectSettingsFactory;
+    this.issueStorage = issueStorage;
   }
 
-  public List<String> listAvailableActions(IssueDto issue) {
-    List<String> actions = newArrayList();
+  public List<String> listAvailableActions(String issueKey) {
+    DbSession session = dbClient.openSession(false);
+    try {
+      return listAvailableActions(issueService.getByKeyForUpdate(session, issueKey).toDefaultIssue());
+    } finally {
+      dbClient.closeSession(session);
+    }
+  }
+
+  public List<String> listAvailableActions(Issue issue) {
+    List<String> availableActions = newArrayList();
     String login = userSession.getLogin();
     if (login != null) {
-      actions.add("comment");
-      if (issue.getResolution() == null) {
-        actions.add("assign");
-        actions.add("set_tags");
-        if (!login.equals(issue.getAssignee())) {
-          actions.add("assign_to_me");
+      availableActions.add("comment");
+      if (issue.resolution() == null) {
+        availableActions.add("assign");
+        availableActions.add("set_tags");
+        if (!login.equals(issue.assignee())) {
+          availableActions.add("assign_to_me");
         }
-        actions.add("plan");
-        String projectUuid = issue.getProjectUuid();
-        if (projectUuid != null && userSession.hasProjectPermissionByUuid(UserRole.ISSUE_ADMIN, projectUuid)) {
-          actions.add("set_severity");
+        availableActions.add("plan");
+        String projectUuid = issue.projectUuid();
+        if (projectUuid != null && userSession.hasProjectPermissionByUuid(ISSUE_ADMIN, projectUuid)) {
+          availableActions.add("set_severity");
         }
       }
+      for (String action : loadPluginActions(issue)) {
+        availableActions.add(action);
+      }
+    }
+    return availableActions;
+  }
+
+  private List<String> loadPluginActions(final Issue issue) {
+    return from(actions.list())
+      .filter(new SupportIssue(issue))
+      .transform(ActionToKey.INSTANCE)
+      .toList();
+  }
+
+  public Issue execute(String issueKey, String actionKey) {
+    checkArgument(!Strings.isNullOrEmpty(actionKey), "Missing action");
+
+    DbSession session = dbClient.openSession(false);
+    try {
+      DefaultIssue issue = issueService.getByKeyForUpdate(session, issueKey).toDefaultIssue();
+      Action action = getAction(actionKey, issue);
+
+      IssueChangeContext changeContext = IssueChangeContext.createUser(new Date(), userSession.getLogin());
+      FunctionContext functionContext = new FunctionContext(issue, updater, changeContext, projectSettingsFactory.newProjectSettings(issue.projectKey()));
+      for (Function function : action.functions()) {
+        function.execute(functionContext);
+      }
+      issueStorage.save(issue);
+      return issue;
+    } finally {
+      dbClient.closeSession(session);
+    }
+  }
+
+  private Action getAction(String actionKey, Issue issue) {
+    Action action = find(actions.list(), new MatchActionKey(actionKey), null);
+    checkArgument(action != null, String.format("Action is not found : %s", actionKey));
+    checkState(action.supports(issue), "A condition is not respected");
+    return action;
+  }
+
+  static class FunctionContext implements Function.Context {
+
+    private final DefaultIssue issue;
+    private final IssueUpdater updater;
+    private final IssueChangeContext changeContext;
+    private final Settings projectSettings;
+
+    FunctionContext(DefaultIssue issue, IssueUpdater updater, IssueChangeContext changeContext, Settings projectSettings) {
+      this.updater = updater;
+      this.issue = issue;
+      this.changeContext = changeContext;
+      this.projectSettings = projectSettings;
+    }
+
+    @Override
+    public Issue issue() {
+      return issue;
+    }
+
+    @Override
+    public Settings projectSettings() {
+      return projectSettings;
+    }
+
+    @Override
+    public Function.Context setAttribute(String key, @Nullable String value) {
+      updater.setAttribute(issue, key, value, changeContext);
+      return this;
+    }
+
+    @Override
+    public Function.Context addComment(@Nullable String text) {
+      if (text != null) {
+        updater.addComment(issue, text, changeContext);
+      }
+      return this;
+    }
+  }
+
+  private static class SupportIssue implements Predicate<Action> {
+    private final Issue issue;
+
+    public SupportIssue(Issue issue) {
+      this.issue = issue;
+    }
+
+    @Override
+    public boolean apply(@Nonnull Action action) {
+      return action.supports(issue);
+    }
+  }
+
+  private enum ActionToKey implements com.google.common.base.Function<Action, String> {
+    INSTANCE;
+
+    @Override
+    public String apply(@Nonnull Action action) {
+      return action.key();
+    }
+  }
+
+  private static class MatchActionKey implements Predicate<Action> {
+    private final String actionKey;
+
+    private MatchActionKey(String actionKey) {
+      this.actionKey = actionKey;
+    }
+
+    @Override
+    public boolean apply(@Nonnull Action action) {
+      return action.key().equals(actionKey);
     }
-    return actions;
   }
 }
index 67c580aa7e01f3c8fbab0d7f59b754c98165b087..d8a382025db6343519b0839581ec766d17e310e3 100644 (file)
@@ -82,6 +82,7 @@ public class InternalRubyIssueService {
   private final ResourceDao resourceDao;
   private final IssueFilterService issueFilterService;
   private final IssueBulkChangeService issueBulkChangeService;
+  private final ActionService actionService;
   private final UserSession userSession;
 
   public InternalRubyIssueService(
@@ -91,7 +92,7 @@ public class InternalRubyIssueService {
     IssueChangelogService changelogService, ActionPlanService actionPlanService,
     ResourceDao resourceDao,
     IssueFilterService issueFilterService, IssueBulkChangeService issueBulkChangeService,
-    UserSession userSession) {
+    ActionService actionService, UserSession userSession) {
     this.issueService = issueService;
     this.issueQueryService = issueQueryService;
     this.commentService = commentService;
@@ -100,6 +101,7 @@ public class InternalRubyIssueService {
     this.resourceDao = resourceDao;
     this.issueFilterService = issueFilterService;
     this.issueBulkChangeService = issueBulkChangeService;
+    this.actionService = actionService;
     this.userSession = userSession;
   }
 
@@ -314,6 +316,20 @@ public class InternalRubyIssueService {
     return result;
   }
 
+  public Result<Issue> executeAction(String issueKey, String actionKey) {
+    Result<Issue> result = Result.of();
+    try {
+      result.set(actionService.execute(issueKey, actionKey));
+    } catch (Exception e) {
+      result.addError(e.getMessage());
+    }
+    return result;
+  }
+
+  public List<String> listActions(String issueKey) {
+    return actionService.listAvailableActions(issueKey);
+  }
+
   public IssueQuery emptyIssueQuery() {
     return issueQueryService.createFromMap(Maps.<String, Object>newHashMap());
   }
index 8660724f7f158ec5365eade7499069977c20edf6..306b322f7500c26b6509f7d4cbea18f9ba939058 100644 (file)
 
 package org.sonar.server.issue.ws;
 
-import java.util.List;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.server.ServerSide;
 import org.sonar.api.utils.text.JsonWriter;
-import org.sonar.api.web.UserRole;
 import org.sonar.core.issue.workflow.Transition;
+import org.sonar.server.issue.ActionService;
 import org.sonar.server.issue.IssueService;
 import org.sonar.server.user.UserSession;
 
-import static com.google.common.collect.Lists.newArrayList;
-
 @ServerSide
 public class IssueActionsWriter {
 
   private final IssueService issueService;
+  private final ActionService actionService;
   private final UserSession userSession;
 
-  public IssueActionsWriter(IssueService issueService, UserSession userSession) {
+  public IssueActionsWriter(IssueService issueService, ActionService actionService, UserSession userSession) {
     this.issueService = issueService;
+    this.actionService = actionService;
     this.userSession = userSession;
   }
 
@@ -54,31 +53,10 @@ public class IssueActionsWriter {
 
   public void writeActions(Issue issue, JsonWriter json) {
     json.name("actions").beginArray();
-    for (String action : actions(issue)) {
+    for (String action : actionService.listAvailableActions(issue)) {
       json.value(action);
     }
     json.endArray();
   }
 
-  private List<String> actions(Issue issue) {
-    List<String> actions = newArrayList();
-    String login = userSession.getLogin();
-    if (login != null) {
-      actions.add("comment");
-      if (issue.resolution() == null) {
-        actions.add("assign");
-        actions.add("set_tags");
-        if (!login.equals(issue.assignee())) {
-          actions.add("assign_to_me");
-        }
-        actions.add("plan");
-        String projectUuid = issue.projectUuid();
-        if (projectUuid != null && userSession.hasProjectPermissionByUuid(UserRole.ISSUE_ADMIN, projectUuid)) {
-          actions.add("set_severity");
-        }
-      }
-    }
-    return actions;
-  }
-
 }
index 8ac665b2b7b9783afd6447c3b9c20263962c9886..d5911cc20116e8330b50a687438884b1047b2c51 100644 (file)
@@ -35,6 +35,7 @@ public class IssuesWs implements WebService {
   public static final String EDIT_COMMENT_ACTION = "edit_comment";
   public static final String TRANSITIONS_ACTION = "transitions";
   public static final String BULK_CHANGE_ACTION = "bulk_change";
+  public static final String DO_ACTION_ACTION = "do_action";
 
   private final IssuesWsAction[] actions;
 
@@ -61,6 +62,7 @@ public class IssuesWs implements WebService {
     defineEditCommentAction(controller);
     defineTransitionsAction(controller);
     defineBulkChangeAction(controller);
+    defineDoActionAction(controller);
   }
 
   private static void defineChangelogAction(NewController controller) {
@@ -176,4 +178,21 @@ public class IssuesWs implements WebService {
     RailsHandler.addFormatParam(action);
   }
 
+  private static void defineDoActionAction(NewController controller) {
+    WebService.NewAction action = controller.createAction(DO_ACTION_ACTION)
+      .setDescription("Do workflow transition on an issue. Requires authentication and Browse permission on project")
+      .setSince("3.6")
+      .setHandler(RailsHandler.INSTANCE)
+      .setPost(true);
+
+    action.createParam("issue")
+      .setDescription("Key of the issue")
+      .setRequired(true)
+      .setExampleValue("5bccd6e8-f525-43a2-8d76-fcb13dde79ef");
+    action.createParam("actionKey")
+      .setDescription("Action to perform")
+      .setExampleValue("link-to-jira");
+    RailsHandler.addFormatParam(action);
+  }
+
 }
index ec1b2257d619379d6bc056a12c53442ee5a975a0..ba7ba678e19be25e00c099aa184a179ed174d1c6 100644 (file)
@@ -138,7 +138,6 @@ public class SearchResponseFormat {
       if (fields.contains(SearchAdditionalField.COMMENTS)) {
         formatIssueComments(data, issueBuilder, dto);
       }
-      // TODO attributes
       result.add(issueBuilder.build());
     }
     return result;
index 74d8a2146594e9f8b31a5d4e5c134b5e97b0ba18..ac510fe381b45022de3cd42b01c81b639653807a 100644 (file)
@@ -139,7 +139,7 @@ public class SearchResponseLoader {
       for (IssueDto dto : result.getIssues()) {
         // so that IssueDto can be used.
         if (collector.contains(ACTIONS)) {
-          result.addActions(dto.getKey(), actionService.listAvailableActions(dto));
+          result.addActions(dto.getKey(), actionService.listAvailableActions(dto.toDefaultIssue()));
         }
         if (collector.contains(TRANSITIONS)) {
           // TODO workflow and action engines must not depend on org.sonar.api.issue.Issue but on a generic interface
diff --git a/server/sonar-server/src/main/java/org/sonar/server/properties/ProjectSettings.java b/server/sonar-server/src/main/java/org/sonar/server/properties/ProjectSettings.java
deleted file mode 100644 (file)
index 9eb94f9..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2014 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.properties;
-
-import org.apache.commons.lang.StringUtils;
-import org.sonar.api.config.Settings;
-
-import java.util.Map;
-
-public class ProjectSettings extends Settings {
-  private final Settings settings;
-  private final Map<String, String> projectProperties;
-
-  public ProjectSettings(Settings settings, Map<String, String> projectProperties) {
-    this.settings = settings;
-    this.projectProperties = projectProperties;
-  }
-
-  @Override
-  public String getString(String key) {
-    String value = get(key);
-    if (value == null) {
-      return settings.getString(key);
-    }
-
-    return value;
-  }
-
-  @Override
-  public boolean getBoolean(String key) {
-    String value = get(key);
-    if (value == null) {
-      return settings.getBoolean(key);
-    }
-
-    return StringUtils.isNotEmpty(value) && Boolean.parseBoolean(value);
-  }
-
-  @Override
-  public int getInt(String key) {
-    String value = get(key);
-    if (value == null) {
-      return settings.getInt(key);
-    } else if (StringUtils.isNotEmpty(value)) {
-      return Integer.parseInt(value);
-    } else {
-      return 0;
-    }
-  }
-
-  private String get(String key) {
-    return projectProperties.get(key);
-  }
-}
index 43986c1ea3532dc6a4322cc1a6d9e3b6dd2c1ff5..994c3377aaf46ce7ddc8b755cf53ac5b2d465286 100644 (file)
 
 package org.sonar.server.properties;
 
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.Maps;
+import java.util.List;
 import org.sonar.api.config.Settings;
 import org.sonar.api.server.ServerSide;
 import org.sonar.db.property.PropertiesDao;
 import org.sonar.db.property.PropertyDto;
 
-import java.util.List;
-import java.util.Map;
-
 @ServerSide
 public class ProjectSettingsFactory {
 
@@ -43,19 +39,10 @@ public class ProjectSettingsFactory {
 
   public Settings newProjectSettings(String projectKey) {
     List<PropertyDto> propertyList = dao.selectProjectProperties(projectKey);
-
-    return new ProjectSettings(settings, getPropertyMap(propertyList));
-  }
-
-  @VisibleForTesting
-  Map<String, String> getPropertyMap(List<PropertyDto> propertyDtoList) {
-    Map<String, String> propertyMap = Maps.newHashMap();
-    for (PropertyDto property : propertyDtoList) {
-      String key = property.getKey();
-      String value = property.getValue();
-      propertyMap.put(key, value);
+    Settings projectSettings = new Settings(settings);
+    for (PropertyDto property : propertyList) {
+      projectSettings.setProperty(property.getKey(), property.getValue());
     }
-
-    return propertyMap;
+    return projectSettings;
   }
 }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ActionServiceTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ActionServiceTest.java
new file mode 100644 (file)
index 0000000..0d6570b
--- /dev/null
@@ -0,0 +1,238 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.config.Settings;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.action.Actions;
+import org.sonar.api.issue.action.Function;
+import org.sonar.api.issue.condition.Condition;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.IssueChangeContext;
+import org.sonar.core.issue.IssueUpdater;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.server.properties.ProjectSettingsFactory;
+import org.sonar.server.tester.UserSessionRule;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
+import static org.sonar.api.web.UserRole.ISSUE_ADMIN;
+import static org.sonar.db.component.ComponentTesting.newFileDto;
+import static org.sonar.db.component.ComponentTesting.newProjectDto;
+import static org.sonar.db.rule.RuleTesting.newXooX1;
+
+public class ActionServiceTest {
+
+  static final String PROJECT_KEY = "PROJECT_KEY";
+  static final String PROJECT_UUID = "PROJECT_UUID";
+
+  static final String ISSUE_KEY = "ISSUE_KEY";
+  static final String PLUGIN_ACTION = "link-to-jira";
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone().login("arthur");
+
+  DbClient dbClient = mock(DbClient.class);
+  DbSession session = mock(DbSession.class);
+
+  IssueService issueService = mock(IssueService.class);
+  IssueStorage issueStorage = mock(IssueStorage.class);
+  IssueUpdater updater = mock(IssueUpdater.class);
+  ProjectSettingsFactory projectSettingsFactory = mock(ProjectSettingsFactory.class);
+  Settings settings = new Settings();
+  Actions actions = new Actions();
+  ActionService actionService;
+
+  ComponentDto project;
+  IssueDto issue;
+
+  @Before
+  public void before() {
+    when(dbClient.openSession(false)).thenReturn(session);
+    when(projectSettingsFactory.newProjectSettings(PROJECT_KEY)).thenReturn(settings);
+
+    project = newProjectDto(PROJECT_UUID).setKey(PROJECT_KEY);
+    issue = IssueTesting.newDto(newXooX1().setId(10), newFileDto(project), project).setKee(ISSUE_KEY);
+
+    actionService = new ActionService(dbClient, userSession, projectSettingsFactory, actions, issueService, updater, issueStorage);
+  }
+
+  @Test
+  public void execute_functions() {
+    Function function1 = mock(Function.class);
+    Function function2 = mock(Function.class);
+
+    when(issueService.getByKeyForUpdate(session, ISSUE_KEY)).thenReturn(issue);
+
+    actions.add(PLUGIN_ACTION).setConditions(new AlwaysMatch()).setFunctions(function1, function2);
+
+    assertThat(actionService.execute(ISSUE_KEY, PLUGIN_ACTION)).isNotNull();
+
+    verify(function1).execute(any(Function.Context.class));
+    verify(function2).execute(any(Function.Context.class));
+    verifyNoMoreInteractions(function1, function2);
+  }
+
+  @Test
+  public void modify_issue_when_executing_a_function() {
+    Function function = new TweetFunction();
+    when(issueService.getByKeyForUpdate(session, ISSUE_KEY)).thenReturn(issue);
+
+    actions.add(PLUGIN_ACTION).setConditions(new AlwaysMatch()).setFunctions(function);
+    assertThat(actionService.execute(ISSUE_KEY, PLUGIN_ACTION)).isNotNull();
+
+    verify(updater).addComment(any(DefaultIssue.class), eq("New tweet on issue ISSUE_KEY"), any(IssueChangeContext.class));
+    verify(updater).setAttribute(any(DefaultIssue.class), eq("tweet"), eq("tweet sent"), any(IssueChangeContext.class));
+  }
+
+  @Test
+  public void use_project_settings_when_executing_a_function() {
+    Function function = new SettingsFunction();
+    when(issueService.getByKeyForUpdate(session, ISSUE_KEY)).thenReturn(issue);
+    settings.setProperty("key", "value");
+
+    actions.add(PLUGIN_ACTION).setConditions(new AlwaysMatch()).setFunctions(function);
+    actionService.execute(ISSUE_KEY, PLUGIN_ACTION);
+
+    verify(updater).addComment(any(DefaultIssue.class), eq("Property 'key' is 'value'"), any(IssueChangeContext.class));
+  }
+
+  @Test
+  public void not_execute_function_if_action_not_found() {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Action is not found : tweet");
+
+    Function function = mock(Function.class);
+    when(issueService.getByKeyForUpdate(session, ISSUE_KEY)).thenReturn(issue);
+    actions.add(PLUGIN_ACTION).setConditions(new AlwaysMatch()).setFunctions(function);
+    actionService.execute(ISSUE_KEY, "tweet");
+  }
+
+  @Test
+  public void not_execute_function_if_action_is_not_supported() {
+    thrown.expect(IllegalStateException.class);
+    thrown.expectMessage("A condition is not respected");
+
+    Function function = mock(Function.class);
+
+    when(issueService.getByKeyForUpdate(session, ISSUE_KEY)).thenReturn(issue);
+    actions.add(PLUGIN_ACTION).setConditions(new NeverMatch()).setFunctions(function);
+    actionService.execute(ISSUE_KEY, PLUGIN_ACTION);
+  }
+
+  @Test
+  public void return_plugin_actions() {
+    actions.add(PLUGIN_ACTION).setConditions(new AlwaysMatch());
+    actions.add("tweet").setConditions(new NeverMatch());
+    assertThat(actionService.listAvailableActions(issue.toDefaultIssue())).contains(PLUGIN_ACTION);
+  }
+
+  @Test
+  public void return_plugin_actions_on_resolved_issue() {
+    actions.add(PLUGIN_ACTION).setConditions(new AlwaysMatch());
+    actions.add("tweet").setConditions(new NeverMatch());
+    issue = IssueTesting.newDto(newXooX1().setId(10), newFileDto(project), project).setKee(ISSUE_KEY).setResolution(RESOLUTION_FIXED);
+
+    assertThat(actionService.listAvailableActions(issue.toDefaultIssue())).contains(PLUGIN_ACTION);
+  }
+
+  @Test
+  public void return_provided_actions_without_set_severity_when_not_issue_admin() {
+    assertThat(actionService.listAvailableActions(issue.toDefaultIssue())).containsOnly("comment", "assign", "set_tags", "assign_to_me", "plan");
+  }
+
+  @Test
+  public void return_provided_actions_with_set_severity_when_issue_admin() {
+    userSession.addProjectUuidPermissions(ISSUE_ADMIN, PROJECT_UUID);
+    assertThat(actionService.listAvailableActions(issue.toDefaultIssue())).containsOnly("comment", "assign", "set_tags", "assign_to_me", "plan", "set_severity");
+  }
+
+  @Test
+  public void return_no_actions_when_not_logged() {
+    userSession.anonymous();
+    assertThat(actionService.listAvailableActions(issue.toDefaultIssue())).isEmpty();
+  }
+
+  @Test
+  public void doest_not_return_assign_to_me_action_when_issue_already_assigned_to_user() {
+    userSession.login("julien");
+    IssueDto issue = IssueTesting.newDto(newXooX1().setId(10), newFileDto(project), project).setKee(ISSUE_KEY).setAssignee("julien");
+    assertThat(actionService.listAvailableActions(issue.toDefaultIssue())).doesNotContain("assign_to_me");
+  }
+
+  @Test
+  public void return_only_comment_action_when_issue_has_a_resolution() {
+    IssueDto issue = IssueTesting.newDto(newXooX1().setId(10), newFileDto(project), project).setKee(ISSUE_KEY).setResolution(RESOLUTION_FIXED);
+    assertThat(actionService.listAvailableActions(issue.toDefaultIssue())).containsOnly("comment");
+  }
+
+  @Test
+  public void return_actions_by_issue_key() {
+    when(issueService.getByKeyForUpdate(session, ISSUE_KEY)).thenReturn(issue);
+    assertThat(actionService.listAvailableActions(ISSUE_KEY)).isNotEmpty();
+  }
+
+  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");
+    }
+  }
+
+  public class SettingsFunction implements Function {
+    @Override
+    public void execute(Context context) {
+      context.addComment(String.format("Property 'key' is '%s'", context.projectSettings().getString("key")));
+    }
+  }
+
+}
index b184a8282797966c17a26b4e19f0a86be795a2e4..345cc0392494a25f9b6e6b312f57600a35f40189 100644 (file)
@@ -82,6 +82,8 @@ public class InternalRubyIssueServiceTest {
 
   IssueBulkChangeService issueBulkChangeService;
 
+  ActionService actionService;
+
   InternalRubyIssueService service;
 
   @Before
@@ -94,12 +96,13 @@ public class InternalRubyIssueServiceTest {
     resourceDao = mock(ResourceDao.class);
     issueFilterService = mock(IssueFilterService.class);
     issueBulkChangeService = mock(IssueBulkChangeService.class);
+    actionService = mock(ActionService.class);
 
     ResourceDto project = new ResourceDto().setKey("org.sonar.Sample");
     when(resourceDao.selectResource(any(ResourceQuery.class))).thenReturn(project);
 
     service = new InternalRubyIssueService(issueService, issueQueryService, commentService, changelogService, actionPlanService, resourceDao,
-      issueFilterService, issueBulkChangeService, userSessionRule);
+      issueFilterService, issueBulkChangeService, actionService, userSessionRule);
   }
 
   @Test
index 050ce927fe1980ef846ae9becd98a5e188111430..7232fb5b8a5e8e47bdc013d06daf2f67f9a1a0d4 100644 (file)
@@ -30,9 +30,9 @@ import org.mockito.runners.MockitoJUnitRunner;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.utils.text.JsonWriter;
-import org.sonar.api.web.UserRole;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.workflow.Transition;
+import org.sonar.server.issue.ActionService;
 import org.sonar.server.issue.IssueService;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.test.JsonAssert;
@@ -49,107 +49,74 @@ public class IssueActionsWriterTest {
   @Mock
   IssueService issueService;
 
+  @Mock
+  ActionService actionService;
+
   IssueActionsWriter writer;
 
   @Before
   public void setUp() {
-    writer = new IssueActionsWriter(issueService, userSessionRule);
-  }
-
-  @Test
-  public void write_all_standard_actions() {
-    Issue issue = new DefaultIssue()
-      .setKey("ABCD")
-      .setComponentUuid("BCDE")
-      .setComponentKey("sample:src/main/xoo/sample/Sample.xoo")
-      .setProjectUuid("ABCD")
-      .setProjectKey("sample")
-      .setRuleKey(RuleKey.of("squid", "AvoidCycle"));
-
-    userSessionRule.login("john").addProjectUuidPermissions(UserRole.ISSUE_ADMIN, "ABCD");
-
-    testActions(issue,
-      "{\"actions\": " +
-        "[" +
-        "\"comment\", \"assign\", \"set_tags\", \"assign_to_me\", \"plan\", \"set_severity\"\n" +
-        "]}");
+    writer = new IssueActionsWriter(issueService, actionService, userSessionRule);
   }
 
   @Test
-  public void write_only_comment_action() {
+  public void write_actions() {
     Issue issue = new DefaultIssue()
       .setKey("ABCD")
       .setComponentKey("sample:src/main/xoo/sample/Sample.xoo")
       .setProjectKey("sample")
       .setRuleKey(RuleKey.of("squid", "AvoidCycle"))
-      .setResolution("CLOSED");
+      .setAssignee("john");
+    when(actionService.listAvailableActions(issue)).thenReturn(newArrayList("comment"));
 
     userSessionRule.login("john");
 
     testActions(issue,
       "{\"actions\": " +
         "[" +
-        "\"comment\"" +
+        "\"comment\"\n" +
         "]}");
   }
 
   @Test
-  public void write_no_action_if_not_logged() {
+  public void write_transitions() {
     Issue issue = new DefaultIssue()
       .setKey("ABCD")
       .setComponentKey("sample:src/main/xoo/sample/Sample.xoo")
       .setProjectKey("sample")
       .setRuleKey(RuleKey.of("squid", "AvoidCycle"));
 
-    testActions(issue,
-      "{\"actions\": []}");
-  }
-
-  @Test
-  public void write_actions_without_assign_to_me() {
-    Issue issue = new DefaultIssue()
-      .setKey("ABCD")
-      .setComponentKey("sample:src/main/xoo/sample/Sample.xoo")
-      .setProjectKey("sample")
-      .setRuleKey(RuleKey.of("squid", "AvoidCycle"))
-      .setAssignee("john");
-
+    when(issueService.listTransitions(eq(issue))).thenReturn(newArrayList(Transition.create("reopen", "RESOLVED", "REOPEN")));
     userSessionRule.login("john");
 
-    testActions(issue,
-      "{\"actions\": " +
-        "[" +
-        "\"comment\", \"assign\", \"set_tags\", \"plan\"\n" +
-        "]}");
+    testTransitions(issue,
+      "{\"transitions\": [\n" +
+        "        \"reopen\"\n" +
+        "      ]}");
   }
 
   @Test
-  public void write_transitions() {
+  public void write_no_transitions() {
     Issue issue = new DefaultIssue()
       .setKey("ABCD")
       .setComponentKey("sample:src/main/xoo/sample/Sample.xoo")
       .setProjectKey("sample")
       .setRuleKey(RuleKey.of("squid", "AvoidCycle"));
 
-    when(issueService.listTransitions(eq(issue))).thenReturn(newArrayList(Transition.create("reopen", "RESOLVED", "REOPEN")));
     userSessionRule.login("john");
 
     testTransitions(issue,
-      "{\"transitions\": [\n" +
-        "        \"reopen\"\n" +
-        "      ]}");
+      "{\"transitions\": []}");
   }
 
   @Test
-  public void write_no_transitions() {
+  public void write_no_transitions_if_not_logged() {
     Issue issue = new DefaultIssue()
       .setKey("ABCD")
       .setComponentKey("sample:src/main/xoo/sample/Sample.xoo")
       .setProjectKey("sample")
       .setRuleKey(RuleKey.of("squid", "AvoidCycle"));
 
-    userSessionRule.login("john");
-
     testTransitions(issue,
       "{\"transitions\": []}");
   }
index df93f08dd4a9a3889b658116f0b884a0e17bf65a..c592d1f993b7d2ef4031768ca90cf7c97f57a7af 100644 (file)
 
 package org.sonar.server.issue.ws;
 
-import com.google.common.collect.ImmutableMap;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.ClassRule;
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.sonar.api.issue.Issue;
@@ -32,12 +30,12 @@ import org.sonar.api.rule.RuleStatus;
 import org.sonar.api.security.DefaultGroups;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.api.utils.DateUtils;
-import org.sonar.api.utils.KeyValueFormat;
 import org.sonar.api.web.UserRole;
 import org.sonar.core.permission.GlobalPermissions;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ComponentTesting;
 import org.sonar.db.issue.ActionPlanDao;
 import org.sonar.db.issue.ActionPlanDto;
 import org.sonar.db.issue.IssueChangeDao;
@@ -47,7 +45,6 @@ import org.sonar.db.issue.IssueDto;
 import org.sonar.db.rule.RuleDto;
 import org.sonar.db.rule.RuleTesting;
 import org.sonar.db.user.UserDto;
-import org.sonar.db.component.ComponentTesting;
 import org.sonar.server.issue.IssueQuery;
 import org.sonar.server.issue.IssueTesting;
 import org.sonar.server.issue.filter.IssueFilterParameters;
@@ -241,24 +238,6 @@ public class SearchActionMediumTest {
     result.assertJson(this.getClass(), "issue_with_action_plan.json");
   }
 
-  @Ignore("temporarily disabled")
-  @Test
-  public void issue_with_attributes() throws Exception {
-    ComponentDto project = insertComponent(ComponentTesting.newProjectDto("PROJECT_ID").setKey("PROJECT_KEY"));
-    setDefaultProjectPermission(project);
-    ComponentDto file = insertComponent(ComponentTesting.newFileDto(project, "FILE_ID").setKey("FILE_KEY"));
-    IssueDto issue = IssueTesting.newDto(newRule(), file, project)
-      .setKee("82fd47d4-b650-4037-80bc-7b112bd4eac2")
-      .setIssueAttributes(KeyValueFormat.format(ImmutableMap.of("jira-issue-key", "SONAR-1234")));
-    db.issueDao().insert(session, issue);
-    session.commit();
-    tester.get(IssueIndexer.class).indexAll();
-
-    WsTester.Result result = wsTester.newGetRequest(IssuesWs.API_ENDPOINT, SearchAction.SEARCH_ACTION)
-      .execute();
-    result.assertJson(this.getClass(), "issue_with_attributes.json");
-  }
-
   @Test
   public void load_additional_fields() throws Exception {
     db.userDao().insert(session, new UserDto().setLogin("simon").setName("Simon").setEmail("simon@email.com"));
index 661ab46de436ed263c0bc65b05f9c430bc54ac47..5a8aefe66a9f3fba6466d2bc5f0d3ba1a23f7f33 100644 (file)
@@ -51,6 +51,7 @@ import org.sonar.db.component.ComponentDao;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.ComponentTesting;
 import org.sonar.server.debt.DebtModelService;
+import org.sonar.server.issue.ActionService;
 import org.sonar.server.issue.IssueChangelog;
 import org.sonar.server.issue.IssueChangelogService;
 import org.sonar.server.issue.IssueCommentService;
@@ -90,6 +91,9 @@ public class ShowActionTest {
   @Mock
   IssueService issueService;
 
+  @Mock
+  ActionService actionService;
+
   @Mock
   IssueChangelogService issueChangelogService;
 
@@ -142,7 +146,7 @@ public class ShowActionTest {
     tester = new WsTester(new IssuesWs(
       new ShowAction(
         dbClient, issueService, issueChangelogService, commentService,
-        new IssueActionsWriter(issueService, userSessionRule),
+        new IssueActionsWriter(issueService, actionService, userSessionRule),
         actionPlanService, userFinder, debtModel, ruleService, i18n, durations, userSessionRule)));
   }
 
@@ -455,6 +459,8 @@ public class ShowActionTest {
       .setStatus("OPEN");
     when(issueService.getByKey(issue.key())).thenReturn(issue);
 
+    when(actionService.listAvailableActions(issue)).thenReturn(newArrayList( "comment", "assign", "set_tags", "assign_to_me", "plan"));
+
     userSessionRule.login("john");
     WsTester.TestRequest request = tester.newGetRequest("api/issues", "show").setParam("key", issue.key());
     request.execute().assertJson(getClass(), "show_issue_with_actions.json");
diff --git a/server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsFactoryTest.java b/server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsFactoryTest.java
new file mode 100644 (file)
index 0000000..8ceff5e
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.properties;
+
+import org.junit.Test;
+import org.sonar.api.config.Settings;
+import org.sonar.db.property.PropertiesDao;
+import org.sonar.db.property.PropertyDto;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ProjectSettingsFactoryTest {
+
+  static final String PROJECT_KEY = "PROJECT_KEY";
+
+  Settings settings = new Settings();
+  PropertiesDao dao = mock(PropertiesDao.class);
+
+  ProjectSettingsFactory underTest = new ProjectSettingsFactory(settings, dao);
+
+  @Test
+  public void return_global_settings() {
+    settings.setProperty("key", "value");
+    Settings projectSettings = underTest.newProjectSettings(PROJECT_KEY);
+
+    assertThat(projectSettings.getProperties()).hasSize(1);
+    assertThat(projectSettings.getString("key")).isEqualTo("value");
+  }
+
+  @Test
+  public void return_project_settings() {
+    when(dao.selectProjectProperties(PROJECT_KEY)).thenReturn(newArrayList(
+      new PropertyDto().setKey("1").setValue("val1"),
+      new PropertyDto().setKey("2").setValue("val2"),
+      new PropertyDto().setKey("3").setValue("val3"))
+      );
+
+    Settings projectSettings = underTest.newProjectSettings(PROJECT_KEY);
+
+    assertThat(projectSettings.getString("1")).isEqualTo("val1");
+    assertThat(projectSettings.getString("2")).isEqualTo("val2");
+    assertThat(projectSettings.getString("3")).isEqualTo("val3");
+  }
+
+  @Test
+  public void project_settings_override_global_settings() {
+    settings.setProperty("key", "value");
+    when(dao.selectProjectProperties(PROJECT_KEY)).thenReturn(newArrayList(
+        new PropertyDto().setKey("key").setValue("value2"))
+    );
+
+    Settings projectSettings = underTest.newProjectSettings(PROJECT_KEY);
+    assertThat(projectSettings.getString("key")).isEqualTo("value2");
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsRespositoryFactoryTest.java b/server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsRespositoryFactoryTest.java
deleted file mode 100644 (file)
index 59ed79f..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2014 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.properties;
-
-import com.google.common.collect.Lists;
-import org.junit.Before;
-import org.junit.Test;
-import org.sonar.api.config.Settings;
-import org.sonar.db.property.PropertiesDao;
-import org.sonar.db.property.PropertyDto;
-
-import java.util.Map;
-
-import static com.google.common.collect.Lists.newArrayList;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-
-public class ProjectSettingsRespositoryFactoryTest {
-
-  private ProjectSettingsFactory underTest;
-
-  @Before
-  public void before() {
-    Settings settings = mock(Settings.class);
-    PropertiesDao dao = mock(PropertiesDao.class);
-
-    this.underTest = new ProjectSettingsFactory(settings, dao);
-  }
-
-  @Test
-  public void newProjectSettings_returns_a_ProjectSettings() {
-    Settings projectSettings = underTest.newProjectSettings("PROJECT_KEY");
-
-    assertThat(projectSettings).isInstanceOf(ProjectSettings.class);
-  }
-
-  @Test
-  public void transform_empty_list_into_empty_map() {
-    Map<String, String> propertyMap = underTest.getPropertyMap(Lists.<PropertyDto>newArrayList());
-
-    assertThat(propertyMap).isEmpty();
-  }
-
-  @Test
-  public void transform_list_of_properties_in_map_key_value() {
-    PropertyDto property1 = new PropertyDto().setKey("1").setValue("val1");
-    PropertyDto property2 = new PropertyDto().setKey("2").setValue("val2");
-    PropertyDto property3 = new PropertyDto().setKey("3").setValue("val3");
-
-    Map<String, String> propertyMap = underTest.getPropertyMap(newArrayList(property1, property2, property3));
-
-    assertThat(propertyMap.get("1")).isEqualTo("val1");
-    assertThat(propertyMap.get("2")).isEqualTo("val2");
-    assertThat(propertyMap.get("3")).isEqualTo("val3");
-  }
-}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsRespositoryTest.java b/server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsRespositoryTest.java
deleted file mode 100644 (file)
index 6ea1e58..0000000
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2014 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.properties;
-
-import com.google.common.collect.Maps;
-import org.junit.Before;
-import org.junit.Test;
-import org.sonar.api.config.Settings;
-
-import java.util.HashMap;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-public class ProjectSettingsRespositoryTest {
-
-  private ProjectSettings underTest;
-
-  private Settings settings;
-
-  @Before
-  public void before() {
-    this.settings = mock(Settings.class);
-  }
-
-  @Test
-  public void call_global_settings_method_when_no_project_specific_settings() {
-    this.underTest = new ProjectSettings(settings, Maps.<String, String>newHashMap());
-
-    underTest.getInt("anyKey");
-    underTest.getBoolean("anyKey");
-    underTest.getString("anyKey");
-
-    verify(settings, times(1)).getBoolean(anyString());
-    verify(settings, times(1)).getInt(anyString());
-    verify(settings, times(1)).getString(anyString());
-  }
-
-  @Test(expected = NumberFormatException.class)
-  public void getInt_property_throws_exception_when_value_is_not_formatted_correctly() {
-    HashMap<String, String> properties = Maps.newHashMap();
-    properties.put("intKey", "wrongIntValue");
-    this.underTest = new ProjectSettings(settings, properties);
-
-    underTest.getInt("intKey");
-  }
-
-  @Test
-  public void getInt_property_return_0_when_empty_property() {
-    HashMap<String, String> properties = Maps.newHashMap();
-    properties.put("intKey", "");
-    this.underTest = new ProjectSettings(settings, properties);
-
-    int value = underTest.getInt("intKey");
-
-    assertThat(value).isEqualTo(0);
-  }
-
-  @Test
-  public void getInt_property_return_the_int_value() {
-    HashMap<String, String> properties = Maps.newHashMap();
-    properties.put("intKey", "123");
-    this.underTest = new ProjectSettings(settings, properties);
-
-    int value = underTest.getInt("intKey");
-
-    assertThat(value).isEqualTo(123);
-  }
-
-  @Test
-  public void getString_returns_String_property() {
-    HashMap<String, String> properties = Maps.newHashMap();
-    properties.put("stringKey", "stringValue");
-    this.underTest = new ProjectSettings(settings, properties);
-
-    String value = underTest.getString("stringKey");
-
-    assertThat(value).isEqualTo("stringValue");
-  }
-
-  @Test
-  public void getBoolean_returns_exception_when_value_is_not_formatted_correctly() {
-    HashMap<String, String> properties = Maps.newHashMap();
-    properties.put("boolKey", "wronglyFormattedBoolean");
-    this.underTest = new ProjectSettings(settings, properties);
-
-    boolean key = underTest.getBoolean("boolKey");
-
-    assertThat(key).isFalse();
-  }
-
-  @Test
-  public void getBoolean_returns_false_when_value_is_empty() {
-    HashMap<String, String> properties = Maps.newHashMap();
-    properties.put("boolKey", "");
-    this.underTest = new ProjectSettings(settings, properties);
-
-    boolean key = underTest.getBoolean("boolKey");
-
-    assertThat(key).isFalse();
-  }
-
-  @Test
-  public void getBoolean_returns_true_when_value_is_true_ignoring_case() {
-    HashMap<String, String> properties = Maps.newHashMap();
-    properties.put("boolKey1", "true");
-    properties.put("boolKey2", "True");
-    this.underTest = new ProjectSettings(settings, properties);
-
-    boolean key1 = underTest.getBoolean("boolKey1");
-    boolean key2 = underTest.getBoolean("boolKey2");
-
-    assertThat(key1).isTrue();
-    assertThat(key2).isTrue();
-  }
-}
index 0827d1f204e085c10449b36b6767aa6fb42137a6..8c143219aa25ce5f130187c99c5b1c73f20581f7 100644 (file)
@@ -12,7 +12,6 @@
     "creationDate": "2014-01-22T19:10:03+0100",
     "fCreationDate": "Jan 22, 2014 10:03 AM",
     "transitions": [],
-    "actions": ["comment", "assign", "set_tags", "assign_to_me", "plan"],
     "comments": [
       {
         "key": "COMMENT-ABCD",
index 9d06cd9d9425a240427fd15551ff38edfc865dd8..fb2e1b2355f28bee0395d4802356669ccbd9c51c 100644 (file)
@@ -14,7 +14,6 @@
     "creationDate": "2014-01-22T19:10:03+0100",
     "fCreationDate": "Jan 22, 2014 10:03 AM",
     "transitions": ["reopen"],
-    "actions": ["comment"],
     "comments": [],
     "changelog": [
       {
index deb21bae193a6a655dbd53b0f7eed96f776ddb19..e6488d26abce4a50823e4fa736748356cb7bd117 100644 (file)
@@ -140,14 +140,35 @@ class Api::IssuesController < Api::ApiController
   def actions
     require_parameters :issue
     issue_key = params[:issue]
-    actions = Internal.issues.listActions(issue_key)
     render :json => jsonp(
       {
-        :actions => actions.map { |t| t.key() }
+        :actions => Internal.issues.listActions(issue_key)
       }
     )
   end
 
+  #
+  # POST /api/issues/do_action?issue=<key>&actionKey=<action key>
+  #
+  # -- Example
+  # curl -X POST -v -u admin:admin 'http://localhost:9000/api/issues/do_action?issue=9b6f89c0-3347-46f6-a6d1-dd6c761240e0&actionKey=link-to-jira'
+  #
+  def do_action
+    verify_post_request
+    require_parameters :issue, :actionKey
+
+    result = Internal.issues.executeAction(params[:issue], params[:actionKey])
+
+    http_status = (result.ok ? 200 : 400)
+    hash = result_to_hash(result)
+
+    respond_to do |format|
+      # if the request header "Accept" is "*/*", then the default format is the first one (json)
+      format.json { render :json => jsonp(hash), :status => result.httpStatus }
+      format.xml { render :xml => hash.to_xml(:skip_types => true, :root => 'sonar', :status => http_status) }
+    end
+  end
+
   #
   # Execute a bulk change on a list of issues
   #
index 4a7254cfabee5677a2fdec7633a6f8e5b342db46..46f6dabbf4e7b4fa31fa47e225430c4592e7d7d3 100644 (file)
@@ -30,9 +30,7 @@ import static com.google.common.collect.Lists.newArrayList;
 
 /**
  * @since 3.6
- * @deprecated in 5.2. Webapp can not be customized anymore to define actions on issues.
  */
-@Deprecated
 public class Action {
 
   private final String key;
index 3386cd33cf90ed7459d6d091d8a07b4c4301345d..813a3e9099048725e02235ab56fe3221ccf3aff3 100644 (file)
@@ -25,22 +25,20 @@ import org.sonar.api.server.ServerSide;
 
 /**
  * @since 3.6
- * @deprecated in 5.2. Webapp can not be customized anymore to define actions on issues.
  */
-@Deprecated
 @ServerSide
 public class Actions {
 
-  private final List<Action> actions = new ArrayList<>();
+  private final List<Action> list = new ArrayList<>();
 
   public Action add(String actionKey) {
     Action action = new Action(actionKey);
-    this.actions.add(action);
+    this.list.add(action);
     return action;
   }
 
   public List<Action> list() {
-    return actions;
+    return list;
   }
 
 }