Browse Source

SONAR-6717 Restore issues actions API

tags/5.2-RC1
Julien Lancelot 8 years ago
parent
commit
f59052c8e1
30 changed files with 1034 additions and 441 deletions
  1. 40
    0
      it/it-plugins/issue-action-plugin/pom.xml
  2. 46
    0
      it/it-plugins/issue-action-plugin/src/main/java/ActionDefinition.java
  3. 34
    0
      it/it-plugins/issue-action-plugin/src/main/java/IssueActionPlugin.java
  4. 21
    0
      it/it-plugins/issue-action-plugin/src/main/resources/org/sonar/l10n/issueaction.properties
  5. 1
    0
      it/it-plugins/pom.xml
  6. 257
    0
      it/it-tests/src/test/java/issue/suite/IssueActionTest.java
  7. 18
    8
      it/it-tests/src/test/java/issue/suite/IssueTestSuite.java
  8. 20
    0
      it/it-tests/src/test/java/util/ItUtils.java
  9. 12
    0
      it/it-tests/src/test/resources/issue/suite/IssueActionTest/xoo-one-issue-per-line-profile.xml
  10. 168
    16
      server/sonar-server/src/main/java/org/sonar/server/issue/ActionService.java
  11. 17
    1
      server/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java
  12. 5
    27
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueActionsWriter.java
  13. 19
    0
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssuesWs.java
  14. 0
    1
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java
  15. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseLoader.java
  16. 0
    72
      server/sonar-server/src/main/java/org/sonar/server/properties/ProjectSettings.java
  17. 5
    18
      server/sonar-server/src/main/java/org/sonar/server/properties/ProjectSettingsFactory.java
  18. 238
    0
      server/sonar-server/src/test/java/org/sonar/server/issue/ActionServiceTest.java
  19. 4
    1
      server/sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java
  20. 18
    51
      server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueActionsWriterTest.java
  21. 1
    22
      server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java
  22. 7
    1
      server/sonar-server/src/test/java/org/sonar/server/issue/ws/ShowActionTest.java
  23. 76
    0
      server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsFactoryTest.java
  24. 0
    74
      server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsRespositoryFactoryTest.java
  25. 0
    137
      server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsRespositoryTest.java
  26. 0
    1
      server/sonar-server/src/test/resources/org/sonar/server/issue/ws/ShowActionTest/show_issue_with_comments.json
  27. 0
    1
      server/sonar-server/src/test/resources/org/sonar/server/issue/ws/ShowActionTest/show_issue_with_transitions.json
  28. 23
    2
      server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/issues_controller.rb
  29. 0
    2
      sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Action.java
  30. 3
    5
      sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Actions.java

+ 40
- 0
it/it-plugins/issue-action-plugin/pom.xml View File

@@ -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>

+ 46
- 0
it/it-plugins/issue-action-plugin/src/main/java/ActionDefinition.java View File

@@ -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");
}
});
}
}

+ 34
- 0
it/it-plugins/issue-action-plugin/src/main/java/IssueActionPlugin.java View File

@@ -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
);
}

}

+ 21
- 0
it/it-plugins/issue-action-plugin/src/main/resources/org/sonar/l10n/issueaction.properties View File

@@ -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

+ 1
- 0
it/it-plugins/pom.xml View 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>

+ 257
- 0
it/it-tests/src/test/java/issue/suite/IssueActionTest.java View File

@@ -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");
}

}

+ 18
- 8
it/it-tests/src/test/java/issue/suite/IssueTestSuite.java View 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);
}

}

+ 20
- 0
it/it-tests/src/test/java/util/ItUtils.java View 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);
}
}

}

+ 12
- 0
it/it-tests/src/test/resources/issue/suite/IssueActionTest/xoo-one-issue-per-line-profile.xml View File

@@ -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>

+ 168
- 16
server/sonar-server/src/main/java/org/sonar/server/issue/ActionService.java View File

@@ -20,13 +20,32 @@

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;
}
}

+ 17
- 1
server/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java View 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());
}

+ 5
- 27
server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueActionsWriter.java View File

@@ -20,25 +20,24 @@

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;
}

}

+ 19
- 0
server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssuesWs.java View 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);
}

}

+ 0
- 1
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java View 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;

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseLoader.java View 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

+ 0
- 72
server/sonar-server/src/main/java/org/sonar/server/properties/ProjectSettings.java View File

@@ -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);
}
}

+ 5
- 18
server/sonar-server/src/main/java/org/sonar/server/properties/ProjectSettingsFactory.java View File

@@ -20,16 +20,12 @@

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;
}
}

+ 238
- 0
server/sonar-server/src/test/java/org/sonar/server/issue/ActionServiceTest.java View File

@@ -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")));
}
}

}

+ 4
- 1
server/sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java View 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

+ 18
- 51
server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueActionsWriterTest.java View 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\": []}");
}

+ 1
- 22
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java View File

@@ -20,11 +20,9 @@

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"));

+ 7
- 1
server/sonar-server/src/test/java/org/sonar/server/issue/ws/ShowActionTest.java View 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");

+ 76
- 0
server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsFactoryTest.java View File

@@ -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");
}
}

+ 0
- 74
server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsRespositoryFactoryTest.java View File

@@ -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");
}
}

+ 0
- 137
server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsRespositoryTest.java View File

@@ -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();
}
}

+ 0
- 1
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/ShowActionTest/show_issue_with_comments.json View 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",

+ 0
- 1
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/ShowActionTest/show_issue_with_transitions.json View 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": [
{

+ 23
- 2
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/issues_controller.rb View 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
#

+ 0
- 2
sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Action.java View 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;

+ 3
- 5
sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Actions.java View 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;
}

}

Loading…
Cancel
Save