]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-3714 Refactor the bulk change service in order to inject Actions instead of...
authorJulien Lancelot <julien.lancelot@gmail.com>
Mon, 24 Jun 2013 17:15:33 +0000 (19:15 +0200)
committerJulien Lancelot <julien.lancelot@gmail.com>
Mon, 24 Jun 2013 17:15:33 +0000 (19:15 +0200)
14 files changed:
sonar-server/src/main/java/org/sonar/server/issue/Action.java [new file with mode: 0644]
sonar-server/src/main/java/org/sonar/server/issue/AssignAction.java [new file with mode: 0644]
sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java
sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeQuery.java
sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeService.java
sonar-server/src/main/java/org/sonar/server/issue/PlanAction.java [new file with mode: 0644]
sonar-server/src/main/java/org/sonar/server/issue/SetSeverityAction.java [new file with mode: 0644]
sonar-server/src/main/java/org/sonar/server/platform/Platform.java
sonar-server/src/test/java/org/sonar/server/issue/AssignActionTest.java [new file with mode: 0644]
sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java
sonar-server/src/test/java/org/sonar/server/issue/IssueBulkChangeQueryTest.java
sonar-server/src/test/java/org/sonar/server/issue/IssueBulkChangeServiceTest.java
sonar-server/src/test/java/org/sonar/server/issue/PlanActionTest.java [new file with mode: 0644]
sonar-server/src/test/java/org/sonar/server/issue/SetSeverityActionTest.java [new file with mode: 0644]

diff --git a/sonar-server/src/main/java/org/sonar/server/issue/Action.java b/sonar-server/src/main/java/org/sonar/server/issue/Action.java
new file mode 100644 (file)
index 0000000..eddfba1
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.issue;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import org.sonar.api.ServerComponent;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.condition.Condition;
+import org.sonar.api.issue.internal.IssueChangeContext;
+import org.sonar.core.issue.IssueUpdater;
+import org.sonar.server.user.UserSession;
+
+import java.util.List;
+import java.util.Map;
+
+import static com.google.common.collect.Lists.newArrayList;
+
+/**
+ * @since 3.7
+ */
+public abstract class Action implements ServerComponent {
+
+  private final String key;
+  private final List<Condition> conditions;
+
+  public Action(String key) {
+    Preconditions.checkArgument(!Strings.isNullOrEmpty(key), "Action key must be set");
+    this.key = key;
+    this.conditions = newArrayList();
+  }
+
+  public String key() {
+    return key;
+  }
+
+  public Action setConditions(Condition... conditions) {
+    this.conditions.addAll(ImmutableList.copyOf(conditions));
+    return this;
+  }
+
+  public List<Condition> conditions() {
+    return conditions;
+  }
+
+  public boolean supports(Issue issue) {
+    for (Condition condition : conditions) {
+      if (!condition.matches(issue)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  abstract boolean verify(Map<String, Object> properties, UserSession userSession);
+
+  abstract boolean execute(Map<String, Object> properties, Context context);
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    Action that = (Action) o;
+    if (!key.equals(that.key)) {
+      return false;
+    }
+    return true;
+  }
+
+  @Override
+  public int hashCode() {
+    return key.hashCode();
+  }
+
+  @Override
+  public String toString() {
+    return key;
+  }
+
+  interface Context {
+    Issue issue();
+
+    IssueUpdater issueUpdater();
+
+    IssueChangeContext issueChangeContext();
+  }
+
+}
+
diff --git a/sonar-server/src/main/java/org/sonar/server/issue/AssignAction.java b/sonar-server/src/main/java/org/sonar/server/issue/AssignAction.java
new file mode 100644 (file)
index 0000000..04a0f82
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.issue;
+
+import org.sonar.api.ServerComponent;
+import org.sonar.api.issue.condition.IsUnResolved;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.user.UserFinder;
+import org.sonar.server.user.UserSession;
+
+import java.util.Map;
+
+
+public class AssignAction extends Action implements ServerComponent {
+
+  public static final String ASSIGN_ACTION_KEY = "assign";
+
+  private final UserFinder userFinder;
+
+  public AssignAction(UserFinder userFinder) {
+    super(ASSIGN_ACTION_KEY);
+    this.userFinder = userFinder;
+    super.setConditions(new IsUnResolved());
+  }
+
+  @Override
+  public boolean verify(Map<String, Object> properties, UserSession userSession){
+    String assignee = assignee(properties);
+    if (assignee != null && userFinder.findByLogin(assignee) == null) {
+      throw new IllegalArgumentException("Unknown user: " + assignee);
+    }
+    return true;
+  }
+
+  @Override
+  public boolean execute(Map<String, Object> properties, Context context) {
+    context.issueUpdater().assign((DefaultIssue) context.issue(), assignee(properties), context.issueChangeContext());
+    return true;
+  }
+
+  private String assignee(Map<String, Object> properties){
+    return (String) properties.get("assignee");
+  }
+}
\ No newline at end of file
index 11ebef8736f553ecc8c59642aad178cae6d07cbe..a3edbcc346a6991d0d4c12290b17be3f5b4ed5aa 100644 (file)
@@ -591,7 +591,7 @@ public class InternalRubyIssueService implements ServerComponent {
   public Result<List<Issue>> executeBulkChange(Map<String, Object> props) {
     Result<List<Issue>> result = Result.of();
     try {
-      IssueBulkChangeQuery issueBulkChangeQuery = toIssueBulkChangeQuery(props);
+      IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(props);
       result.set(issueBulkChangeService.execute(issueBulkChangeQuery, UserSession.get()));
     } catch (Exception e) {
       result.addError(e.getMessage());
@@ -599,15 +599,4 @@ public class InternalRubyIssueService implements ServerComponent {
     return result;
   }
 
-  private IssueBulkChangeQuery toIssueBulkChangeQuery(Map<String, Object> props) {
-    return IssueBulkChangeQuery.builder()
-      .issueKeys(RubyUtils.toStrings(props.get("issues")))
-      .assignee((String) props.get("assignee"))
-      .plan((String) props.get("plan"))
-      .severity((String) props.get("severity"))
-      .transition((String) props.get("transition"))
-      .comment((String) props.get("comment"))
-      .build();
-  }
-
 }
\ No newline at end of file
index 6d1a0e4f9fd3e44b7d154b4e5602eef136ac919e..42324b1315dc6576ce4fbc1b8b177fe285ea5999 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-package org.sonar.server.issue;
 
-import com.google.common.base.Preconditions;
-import org.apache.commons.lang.builder.ReflectionToStringBuilder;
+package org.sonar.server.issue;
 
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.server.util.RubyUtils;
 
-import java.util.Collection;
-import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
-import static com.google.common.collect.Lists.newArrayList;
+import static com.google.common.collect.Maps.newHashMap;
 
 /**
  * @since 3.7
  */
 public class IssueBulkChangeQuery {
 
-  private static final String ASSIGNEE = "assign";
-  private static final String PLAN = "plan";
-  private static final String SEVERITY = "set_severity";
-  private static final String TRANSITION = "do_transition";
-  private static final String COMMENT = "comment";
-
-  private final Collection<String> actions;
-  private final Collection<String> issueKeys;
-  private final String assignee;
-  private final String plan;
-  private final String severity;
-  private final String transition;
-  private final String comment;
-
-  private IssueBulkChangeQuery(Builder builder) {
-    this.actions = defaultCollection(builder.actions);
-    this.issueKeys = defaultCollection(builder.issueKeys);
-    this.assignee = builder.assignee;
-    this.plan = builder.plan;
-    this.severity = builder.severity;
-    this.transition = builder.transition;
-    this.comment = builder.comment;
-  }
-
-  public Collection<String> issueKeys() {
-    return issueKeys;
-  }
-
-  @CheckForNull
-  public String assignee() {
-    return assignee;
-  }
+  private List<String> issues;
+  private List<String> actions;
+  private String comment;
 
-  @CheckForNull
-  public String plan() {
-    return plan;
-  }
+  Map<String, Map<String, Object>> propertiesByActions = new HashMap<String, Map<String, Object>>();
 
-  @CheckForNull
-  public String severity() {
-    return severity;
+  public IssueBulkChangeQuery(Map<String, Object> props) {
+    parse(props, null);
   }
 
-  @CheckForNull
-  public String transition() {
-    return transition;
+  public IssueBulkChangeQuery(Map<String, Object> props, String comment) {
+    parse(props, comment);
   }
 
-  @CheckForNull
-  public String comment() {
-    return comment;
-  }
-
-  public boolean isOnAssignee() {
-    return actions.contains(ASSIGNEE);
-  }
-
-  public boolean isOnActionPlan() {
-    return actions.contains(PLAN);
-  }
-
-  public boolean isOnSeverity() {
-    return actions.contains(SEVERITY);
+  private void parse(Map<String, Object> props, String comment) {
+    this.comment = comment;
+    this.issues = RubyUtils.toStrings(props.get("issues"));
+    if (issues == null || issues.isEmpty()) {
+      throw new IllegalArgumentException("Issues must not be empty");
+    }
+    actions = RubyUtils.toStrings(props.get("actions"));
+    if (actions == null || actions.isEmpty()) {
+      throw new IllegalArgumentException("At least one action must be provided");
+    }
+    for (String action : actions) {
+      propertiesByActions.put(action, getActionProps(action, props));
+    }
   }
 
-  public boolean isOnTransition() {
-    return actions.contains(TRANSITION);
+  public List<String> issues() {
+    return issues;
   }
 
-  public boolean isOnComment() {
-    return actions.contains(COMMENT);
+  public List<String> actions() {
+    return actions;
   }
 
-  @Override
-  public String toString() {
-    return ReflectionToStringBuilder.toString(this);
+  public Map<String, Object> properties(String action) {
+    return propertiesByActions.get(action);
   }
 
-  public static Builder builder() {
-    return new Builder();
+  public String getComment() {
+    return comment;
   }
 
-  public static class Builder {
-
-    private Collection<String> actions = newArrayList();
-    private Collection<String> issueKeys;
-    private String assignee;
-    private String plan;
-    private String severity;
-    private String transition;
-    private String comment;
-
-    private Builder() {
-    }
-
-    public Builder issueKeys(@Nullable Collection<String> l) {
-      this.issueKeys = l;
-      return this;
-    }
-
-    public Builder assignee(@Nullable String assignee) {
-      actions.add(IssueBulkChangeQuery.ASSIGNEE);
-      this.assignee = assignee;
-      return this;
-    }
-
-    public Builder plan(@Nullable String plan) {
-      actions.add(IssueBulkChangeQuery.PLAN);
-      this.plan = plan;
-      return this;
-    }
-
-    public Builder severity(@Nullable String severity) {
-      actions.add(IssueBulkChangeQuery.SEVERITY);
-      this.severity = severity;
-      return this;
+  private static Map<String, Object> getActionProps(String action, Map<String, Object> props) {
+    Map<String, Object> actionProps = newHashMap();
+    for (Map.Entry<String, Object> propsEntry : props.entrySet()) {
+      String key = propsEntry.getKey();
+      String actionPrefix = action + ".";
+      String property = StringUtils.substringAfter(key, actionPrefix);
+      if (!property.isEmpty()) {
+        actionProps.put(property, propsEntry.getValue());
+      }
     }
-
-    public Builder transition(@Nullable String transition) {
-      actions.add(IssueBulkChangeQuery.TRANSITION);
-      this.transition = transition;
-      return this;
-    }
-
-    public Builder comment(@Nullable String comment) {
-      actions.add(IssueBulkChangeQuery.COMMENT);
-      this.comment = comment;
-      return this;
-    }
-
-    public IssueBulkChangeQuery build() {
-      Preconditions.checkArgument(issueKeys != null && !issueKeys.isEmpty(), "Issues must not be empty");
-      Preconditions.checkArgument(!actions.isEmpty(), "At least one action must be provided");
-
-      return new IssueBulkChangeQuery(this);
-    }
-  }
-
-  private static <T> Collection<T> defaultCollection(@Nullable Collection<T> c) {
-    return c == null ? Collections.<T>emptyList() : Collections.unmodifiableCollection(c);
+    props.get(action);
+    return actionProps;
   }
 
 }
index 8480c44eff1c0384e4d5c047de71f0a7f505ab18..836eb4774dcfa88f007a5f5dbad2736e956f9d90 100644 (file)
 
 package org.sonar.server.issue;
 
-import com.google.common.base.Strings;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.issue.IssueQuery;
 import org.sonar.api.issue.IssueQueryResult;
 import org.sonar.api.issue.internal.DefaultIssue;
 import org.sonar.api.issue.internal.IssueChangeContext;
-import org.sonar.api.user.UserFinder;
 import org.sonar.api.web.UserRole;
 import org.sonar.core.issue.IssueNotifications;
 import org.sonar.core.issue.IssueUpdater;
 import org.sonar.core.issue.db.IssueStorage;
-import org.sonar.core.issue.workflow.IssueWorkflow;
 import org.sonar.server.user.UserSession;
 
+import javax.annotation.CheckForNull;
+
 import java.util.Date;
 import java.util.List;
 
@@ -41,77 +44,93 @@ import static com.google.common.collect.Lists.newArrayList;
 
 public class IssueBulkChangeService {
 
+  private static final Logger LOG = LoggerFactory.getLogger(IssueBulkChangeService.class);
+
   private final DefaultIssueFinder issueFinder;
-  private final IssueWorkflow workflow;
   private final IssueUpdater issueUpdater;
   private final IssueStorage issueStorage;
   private final IssueNotifications issueNotifications;
-  private final ActionPlanService actionPlanService;
-  private final UserFinder userFinder;
+  private final List<Action> actions;
 
-  public IssueBulkChangeService(DefaultIssueFinder issueFinder, IssueWorkflow workflow, ActionPlanService actionPlanService, UserFinder userFinder,
-                                IssueUpdater issueUpdater, IssueStorage issueStorage, IssueNotifications issueNotifications) {
+  public IssueBulkChangeService(DefaultIssueFinder issueFinder, IssueUpdater issueUpdater, IssueStorage issueStorage, IssueNotifications issueNotifications, List<Action> actions) {
     this.issueFinder = issueFinder;
-    this.workflow = workflow;
     this.issueUpdater = issueUpdater;
     this.issueStorage = issueStorage;
     this.issueNotifications = issueNotifications;
-    this.actionPlanService = actionPlanService;
-    this.userFinder = userFinder;
+    this.actions = actions;
   }
 
   public List<Issue> execute(IssueBulkChangeQuery issueBulkChangeQuery, UserSession userSession) {
     List<Issue> issues = newArrayList();
     verifyLoggedIn(userSession);
 
-    IssueQueryResult issueQueryResult = issueFinder.find(IssueQuery.builder().issueKeys(issueBulkChangeQuery.issueKeys()).requiredRole(UserRole.USER).build());
-
-    String assignee = issueBulkChangeQuery.assignee();
-    if (assignee != null && userFinder.findByLogin(assignee) == null) {
-      throw new IllegalArgumentException("Unknown user: " + assignee);
+    for (String actionName : issueBulkChangeQuery.actions()) {
+      Action action = getAction(actionName);
+      action.verify(issueBulkChangeQuery.properties(actionName), userSession);
     }
 
-    String actionPlanKey = issueBulkChangeQuery.plan();
-    if (!Strings.isNullOrEmpty(actionPlanKey) && actionPlanService.findByKey(actionPlanKey, userSession) == null) {
-      throw new IllegalArgumentException("Unknown action plan: " + actionPlanKey);
-    }
-    String severity = issueBulkChangeQuery.severity();
-    String transition = issueBulkChangeQuery.transition();
-    String comment = issueBulkChangeQuery.comment();
-
-    IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.login());
+    IssueQueryResult issueQueryResult = issueFinder.find(IssueQuery.builder().issueKeys(issueBulkChangeQuery.issues()).requiredRole(UserRole.USER).build());
+    IssueChangeContext issueChangeContext = IssueChangeContext.createUser(new Date(), userSession.login());
     for (Issue issue : issueQueryResult.issues()) {
-      DefaultIssue defaultIssue = (DefaultIssue) issue;
-      try {
-        if (issueBulkChangeQuery.isOnAssignee()) {
-          issueUpdater.assign(defaultIssue, assignee, context);
-        }
-        if (issueBulkChangeQuery.isOnActionPlan()) {
-          issueUpdater.plan(defaultIssue, actionPlanKey, context);
+      for (String actionName : issueBulkChangeQuery.actions()) {
+        try {
+          Action action = getAction(actionName);
+          ActionContext actionContext = new ActionContext(issue, issueUpdater, issueChangeContext);
+          if (action.supports(issue) && action.execute(issueBulkChangeQuery.properties(actionName), actionContext)) {
+            issueStorage.save((DefaultIssue) issue);
+            issueNotifications.sendChanges((DefaultIssue) issue, issueChangeContext, issueQueryResult);
+            issues.add(issue);
+          }
+        } catch (Exception e) {
+          // Do nothing, just go to the next issue
+          LOG.info("An error occur when trying to apply the action : "+ actionName + " on issue : "+ issue.key() + ". This issue has been ignored.", e);
         }
-        if (issueBulkChangeQuery.isOnSeverity()) {
-          issueUpdater.setManualSeverity(defaultIssue, severity, context);
-        }
-        if (issueBulkChangeQuery.isOnTransition()) {
-          workflow.doTransition(defaultIssue, transition, context);
-        }
-        if (issueBulkChangeQuery.isOnComment()) {
-          issueUpdater.addComment(defaultIssue, comment, context);
-        }
-        issueStorage.save(defaultIssue);
-        issueNotifications.sendChanges(defaultIssue, context, issueQueryResult);
-        issues.add(defaultIssue);
-      } catch (Exception e) {
-        // Do nothing, just go to the next issue
       }
     }
     return issues;
   }
 
+  @CheckForNull
+  private Action getAction(final String actionKey) {
+    return Iterables.find(actions, new Predicate<Action>() {
+      @Override
+      public boolean apply(Action action) {
+        return action.key().equals(actionKey);
+      }
+    }, null);
+  }
+
   private void verifyLoggedIn(UserSession userSession) {
     if (!userSession.isLoggedIn()) {
       // must be logged
       throw new IllegalStateException("User is not logged in");
     }
   }
+
+  static class ActionContext implements Action.Context {
+    private final Issue issue;
+    private final IssueUpdater updater;
+    private final IssueChangeContext changeContext;
+
+    ActionContext(Issue issue, IssueUpdater updater, IssueChangeContext changeContext) {
+      this.updater = updater;
+      this.issue = issue;
+      this.changeContext = changeContext;
+    }
+
+    @Override
+    public Issue issue() {
+      return issue;
+    }
+
+    @Override
+    public IssueUpdater issueUpdater() {
+      return updater;
+    }
+
+    @Override
+    public IssueChangeContext issueChangeContext() {
+      return changeContext;
+    }
+  }
 }
diff --git a/sonar-server/src/main/java/org/sonar/server/issue/PlanAction.java b/sonar-server/src/main/java/org/sonar/server/issue/PlanAction.java
new file mode 100644 (file)
index 0000000..c7d9433
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.issue;
+
+import com.google.common.base.Strings;
+import org.sonar.api.ServerComponent;
+import org.sonar.api.issue.condition.IsUnResolved;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.server.user.UserSession;
+
+import java.util.Map;
+
+
+public class PlanAction extends Action implements ServerComponent {
+
+  public static final String PLAN_ACTION_KEY = "plan";
+
+  private final ActionPlanService actionPlanService;
+
+  public PlanAction(ActionPlanService actionPlanService) {
+    super(PLAN_ACTION_KEY);
+    this.actionPlanService = actionPlanService;
+    super.setConditions(new IsUnResolved());
+  }
+
+  @Override
+  public boolean verify(Map<String, Object> properties, UserSession userSession){
+    String actionPlanKey = planKey(properties);
+    if (!Strings.isNullOrEmpty(actionPlanKey) && actionPlanService.findByKey(actionPlanKey, userSession) == null) {
+      throw new IllegalArgumentException("Unknown action plan: " + actionPlanKey);
+    }
+    return true;
+  }
+
+  @Override
+  public boolean execute(Map<String, Object> properties, Context context) {
+    context.issueUpdater().plan((DefaultIssue) context.issue(), planKey(properties), context.issueChangeContext());
+    return true;
+  }
+
+  private String planKey(Map<String, Object> properties){
+    return (String) properties.get("plan");
+  }
+
+}
\ No newline at end of file
diff --git a/sonar-server/src/main/java/org/sonar/server/issue/SetSeverityAction.java b/sonar-server/src/main/java/org/sonar/server/issue/SetSeverityAction.java
new file mode 100644 (file)
index 0000000..8f6ccb6
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.issue;
+
+import org.sonar.api.ServerComponent;
+import org.sonar.api.issue.condition.IsUnResolved;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.server.user.UserSession;
+
+import java.util.Map;
+
+
+public class SetSeverityAction extends Action implements ServerComponent {
+
+  public static final String SET_SEVERITY_ACTION_KEY = "set_severity";
+
+  public SetSeverityAction() {
+    super(SET_SEVERITY_ACTION_KEY);
+    super.setConditions(new IsUnResolved());
+  }
+
+  @Override
+  public boolean verify(Map<String, Object> properties, UserSession userSession) {
+    return true;
+  }
+
+  @Override
+  public boolean execute(Map<String, Object> properties, Context context) {
+    context.issueUpdater().setSeverity((DefaultIssue) context.issue(), severity(properties), context.issueChangeContext());
+    return true;
+  }
+
+  private String severity(Map<String, Object> properties) {
+    return (String) properties.get("severity");
+  }
+}
\ No newline at end of file
index 7d7c5d873c63c7c1b7446686118e39a54d9f91c1..8968b3d06e0810ef20cf14963caeb1bcfa419479 100644 (file)
@@ -277,6 +277,10 @@ public final class Platform {
     servicesContainer.addSingleton(IssueFilterSerializer.class);
     servicesContainer.addSingleton(IssueFilterService.class);
     servicesContainer.addSingleton(IssueBulkChangeService.class);
+    // issues actions
+    servicesContainer.addSingleton(AssignAction.class);
+    servicesContainer.addSingleton(PlanAction.class);
+    servicesContainer.addSingleton(SetSeverityAction.class);
 
     // rules
     servicesContainer.addSingleton(RubyRuleService.class);
diff --git a/sonar-server/src/test/java/org/sonar/server/issue/AssignActionTest.java b/sonar-server/src/test/java/org/sonar/server/issue/AssignActionTest.java
new file mode 100644 (file)
index 0000000..0d7b2af
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.issue;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.issue.internal.IssueChangeContext;
+import org.sonar.api.user.UserFinder;
+import org.sonar.core.issue.IssueUpdater;
+import org.sonar.core.user.DefaultUser;
+import org.sonar.server.user.UserSession;
+
+import java.util.Map;
+
+import static com.google.common.collect.Maps.newHashMap;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.*;
+
+public class AssignActionTest {
+
+  private AssignAction action;
+
+  private final UserFinder userFinder = mock(UserFinder.class);
+
+  @Before
+  public void before(){
+    action = new AssignAction(userFinder);
+  }
+
+  @Test
+  public void should_execute(){
+    String assignee = "arthur";
+
+    Map<String, Object> properties = newHashMap();
+    properties.put("assignee", assignee);
+    DefaultIssue issue = mock(DefaultIssue.class);
+    IssueUpdater issueUpdater = mock(IssueUpdater.class);
+
+    Action.Context context = mock(Action.Context.class);
+    when(context.issueUpdater()).thenReturn(issueUpdater);
+    when(context.issue()).thenReturn(issue);
+
+    action.execute(properties, context);
+    verify(issueUpdater).assign(eq(issue), eq(assignee), any(IssueChangeContext.class));
+  }
+
+  @Test
+  public void should_verify_assignee_exists(){
+    String assignee = "arthur";
+    Map<String, Object> properties = newHashMap();
+    properties.put("assignee", assignee);
+
+    when(userFinder.findByLogin(assignee)).thenReturn(new DefaultUser());
+    assertThat(action.verify(properties, mock(UserSession.class))).isTrue();
+
+    when(userFinder.findByLogin(assignee)).thenReturn(null);
+    try {
+      action.verify(properties, mock(UserSession.class));
+    } catch (Exception e) {
+      assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Unknown user: arthur");
+    }
+  }
+
+  @Test
+  public void should_support_only_unresolved_issues(){
+    assertThat(action.supports(new DefaultIssue().setStatus(Issue.STATUS_OPEN))).isTrue();
+    assertThat(action.supports(new DefaultIssue().setStatus(Issue.STATUS_CLOSED))).isTrue();
+  }
+}
\ No newline at end of file
index 2363f0c016234bf8bbbf1d996b585caf17b6707c..badffa93a4ede8310eb30d7be202b629241ed880 100644 (file)
@@ -537,7 +537,11 @@ public class InternalRubyIssueServiceTest {
   public void should_execute_bulk_change() {
     Map<String, Object> params = newHashMap();
     params.put("issues", newArrayList("ABCD", "EFGH"));
-    params.put("assignee", "arthur");
+    params.put("actions", newArrayList("do_transition", "assign", "set_severity", "plan"));
+    params.put("do_transition.transition", "confirm");
+    params.put("assign.assignee", "arthur");
+    params.put("set_severity.severity", "MINOR");
+    params.put("plan.plan", "3.7");
     service.executeBulkChange(params);
     verify(issueBulkChangeService).execute(any(IssueBulkChangeQuery.class), any(UserSession.class));
   }
@@ -548,7 +552,8 @@ public class InternalRubyIssueServiceTest {
 
     Map<String, Object> params = newHashMap();
     params.put("issues", newArrayList("ABCD", "EFGH"));
-    params.put("assignee", "arthur");
+    params.put("actions", newArrayList("assign"));
+    params.put("assign.assignee", "arthur");
     Result result = service.executeBulkChange(params);
     assertThat(result.ok()).isFalse();
     assertThat(((Result.Message) result.errors().get(0)).text()).contains("Error");
index 55de437d240ac878fcb88c2476ea933c82ffcfb1..3c9a94bb49ff89ea2d303534d4ba52995839ae52 100644 (file)
@@ -23,32 +23,69 @@ package org.sonar.server.issue;
 import org.junit.Test;
 
 import java.util.Collections;
+import java.util.Map;
 
 import static com.google.common.collect.Lists.newArrayList;
+import static com.google.common.collect.Maps.newHashMap;
 import static org.fest.assertions.Assertions.assertThat;
 
 public class IssueBulkChangeQueryTest {
 
   @Test
-  public void test_build(){
-    IssueBulkChangeQuery issueBulkChangeQuery = IssueBulkChangeQuery.builder().issueKeys(newArrayList("ABCD", "EFGH")).assignee("perceval").build();
-    assertThat(issueBulkChangeQuery.issueKeys()).isNotNull();
-    assertThat(issueBulkChangeQuery.assignee()).isNotNull();
+  public void should_create_query(){
+    Map<String, Object> params = newHashMap();
+    params.put("issues", newArrayList("ABCD", "EFGH"));
+    params.put("actions", newArrayList("do_transition", "assign", "set_severity", "plan"));
+    params.put("do_transition.transition", "confirm");
+    params.put("assign.assignee", "arthur");
+    params.put("set_severity.severity", "MINOR");
+    params.put("plan.plan", "3.7");
+
+    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(params);
+    assertThat(issueBulkChangeQuery.actions()).containsOnly("do_transition", "assign", "set_severity", "plan");
+    assertThat(issueBulkChangeQuery.issues()).containsOnly("ABCD", "EFGH");
+  }
+
+  @Test
+  public void should_get_properties_action(){
+    Map<String, Object> params = newHashMap();
+    params.put("issues", newArrayList("ABCD", "EFGH"));
+    params.put("actions", newArrayList("assign"));
+    params.put("assign.assignee", "arthur");
+
+    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(params);
+    assertThat(issueBulkChangeQuery.properties("assign")).hasSize(1);
+    assertThat(issueBulkChangeQuery.properties("assign").get("assignee")).isEqualTo("arthur");
   }
 
   @Test(expected = IllegalArgumentException.class)
   public void fail_to_build_if_no_issue(){
-    IssueBulkChangeQuery.builder().assignee("perceval").build();
+    Map<String, Object> params = newHashMap();
+    params.put("actions", newArrayList("do_transition", "assign", "set_severity", "plan"));
+    new IssueBulkChangeQuery(params);
   }
 
   @Test(expected = IllegalArgumentException.class)
   public void fail_to_build_if_issues_are_empty(){
-    IssueBulkChangeQuery.builder().issueKeys(Collections.<String>emptyList()).assignee("perceval").build();
+    Map<String, Object> params = newHashMap();
+    params.put("issues", Collections.emptyList());
+    params.put("actions", newArrayList("do_transition", "assign", "set_severity", "plan"));
+    new IssueBulkChangeQuery(params);
   }
 
   @Test(expected = IllegalArgumentException.class)
   public void fail_to_build_if_no_action(){
-    IssueBulkChangeQuery.builder().issueKeys(newArrayList("ABCD", "EFGH")).build();
+    Map<String, Object> params = newHashMap();
+    params.put("issues", Collections.emptyList());
+    new IssueBulkChangeQuery(params);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void fail_to_build_if_actions_are_empty(){
+    Map<String, Object> params = newHashMap();
+    params.put("issues", newArrayList("ABCD", "EFGH"));
+    params.put("actions", Collections.emptyList());
+    new IssueBulkChangeQuery(params);
   }
 
 }
index 5237982fcd1ebb11d0b20d67b5095ee99a3c9e9a..85e04a983a3fbc5ed01700fa12549ec08234584f 100644 (file)
@@ -27,19 +27,18 @@ import org.sonar.api.issue.IssueQuery;
 import org.sonar.api.issue.IssueQueryResult;
 import org.sonar.api.issue.internal.DefaultIssue;
 import org.sonar.api.issue.internal.IssueChangeContext;
-import org.sonar.api.user.UserFinder;
-import org.sonar.core.issue.DefaultActionPlan;
 import org.sonar.core.issue.IssueNotifications;
 import org.sonar.core.issue.IssueUpdater;
 import org.sonar.core.issue.db.IssueStorage;
-import org.sonar.core.issue.workflow.IssueWorkflow;
-import org.sonar.core.user.DefaultUser;
 import org.sonar.server.user.UserSession;
 
 import java.util.List;
+import java.util.Map;
 
 import static com.google.common.collect.Lists.newArrayList;
+import static com.google.common.collect.Maps.newHashMap;
 import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.*;
@@ -47,12 +46,9 @@ import static org.mockito.Mockito.*;
 public class IssueBulkChangeServiceTest {
 
   private DefaultIssueFinder finder = mock(DefaultIssueFinder.class);
-  private IssueWorkflow workflow = mock(IssueWorkflow.class);
   private IssueUpdater issueUpdater = mock(IssueUpdater.class);
   private IssueStorage issueStorage = mock(IssueStorage.class);
   private IssueNotifications issueNotifications = mock(IssueNotifications.class);
-  private ActionPlanService actionPlanService = mock(ActionPlanService.class);
-  private UserFinder userFinder = mock(UserFinder.class);
 
   private IssueQueryResult issueQueryResult = mock(IssueQueryResult.class);
   private UserSession userSession = mock(UserSession.class);
@@ -60,8 +56,11 @@ public class IssueBulkChangeServiceTest {
 
   private IssueBulkChangeService service;
 
+  private Action action = mock(Action.class);
+  private List<Action> actions;
+
   @Before
-  public void before(){
+  public void before() {
     when(userSession.isLoggedIn()).thenReturn(true);
     when(userSession.userId()).thenReturn(10);
     when(userSession.login()).thenReturn("fred");
@@ -69,36 +68,28 @@ public class IssueBulkChangeServiceTest {
     when(finder.find(any(IssueQuery.class))).thenReturn(issueQueryResult);
     when(issueQueryResult.issues()).thenReturn(newArrayList((Issue) issue));
 
-    service = new IssueBulkChangeService(finder, workflow, actionPlanService, userFinder, issueUpdater, issueStorage, issueNotifications);
-  }
-
-  @Test
-  public void should_do_bulk_assign(){
-    String assignee = "perceval";
-    when(userFinder.findByLogin(assignee)).thenReturn(new DefaultUser());
+    when(action.key()).thenReturn("assign");
 
-    IssueBulkChangeQuery issueBulkChangeQuery = IssueBulkChangeQuery.builder().issueKeys(newArrayList(issue.key())).assignee(assignee).build();
-    List<Issue> result = service.execute(issueBulkChangeQuery, userSession);
-    assertThat(result).hasSize(1);
+    actions = newArrayList();
+    actions.add(action);
 
-    verify(issueUpdater).assign(eq(issue), eq(assignee), any(IssueChangeContext.class));
-    verifyNoMoreInteractions(issueUpdater);
-    verify(issueStorage).save(eq(issue));
-    verifyNoMoreInteractions(issueStorage);
-    verify(issueNotifications).sendChanges(eq(issue), any(IssueChangeContext.class), eq(issueQueryResult));
-    verifyNoMoreInteractions(issueNotifications);
+    service = new IssueBulkChangeService(finder, issueUpdater, issueStorage, issueNotifications, actions);
   }
 
   @Test
-  public void should_do_bulk_plan(){
-    String actionPlanKey = "EFGH";
-    when(actionPlanService.findByKey(actionPlanKey, userSession)).thenReturn(new DefaultActionPlan());
+  public void should_execute_bulk_change() {
+    Map<String, Object> properties = newHashMap();
+    properties.put("issues", "ABCD");
+    properties.put("actions", "assign");
 
-    IssueBulkChangeQuery issueBulkChangeQuery = IssueBulkChangeQuery.builder().issueKeys(newArrayList(issue.key())).plan(actionPlanKey).build();
+    when(action.supports(any(Issue.class))).thenReturn(true);
+    when(action.execute(anyMap(), any(IssueBulkChangeService.ActionContext.class))).thenReturn(true);
+    when(action.execute(eq(properties), any(IssueBulkChangeService.ActionContext.class))).thenReturn(true);
+
+    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties);
     List<Issue> result = service.execute(issueBulkChangeQuery, userSession);
     assertThat(result).hasSize(1);
 
-    verify(issueUpdater).plan(eq(issue), eq(actionPlanKey), any(IssueChangeContext.class));
     verifyNoMoreInteractions(issueUpdater);
     verify(issueStorage).save(eq(issue));
     verifyNoMoreInteractions(issueStorage);
@@ -107,73 +98,76 @@ public class IssueBulkChangeServiceTest {
   }
 
   @Test
-  public void should_do_bulk_change_severity(){
-    String severity = "MINOR";
+  public void should_not_execute_bulk_if_issue_does_not_support_action() {
+    Map<String, Object> properties = newHashMap();
+    properties.put("issues", "ABCD");
+    properties.put("actions", "assign");
+
+    when(action.supports(any(Issue.class))).thenReturn(false);
 
-    IssueBulkChangeQuery issueBulkChangeQuery = IssueBulkChangeQuery.builder().issueKeys(newArrayList(issue.key())).severity(severity).build();
+    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties);
     List<Issue> result = service.execute(issueBulkChangeQuery, userSession);
-    assertThat(result).hasSize(1);
+    assertThat(result).isEmpty();
 
-    verify(issueUpdater).setManualSeverity(eq(issue), eq(severity), any(IssueChangeContext.class));
-    verifyNoMoreInteractions(issueUpdater);
-    verify(issueStorage).save(eq(issue));
-    verifyNoMoreInteractions(issueStorage);
-    verify(issueNotifications).sendChanges(eq(issue), any(IssueChangeContext.class), eq(issueQueryResult));
-    verifyNoMoreInteractions(issueNotifications);
+    verifyZeroInteractions(issueUpdater);
+    verifyZeroInteractions(issueStorage);
+    verifyZeroInteractions(issueNotifications);
   }
 
   @Test
-  public void should_do_bulk_transition(){
-    String transition = "reopen";
+  public void should_not_execute_bulk_if_action_could_not_be_executed_on_issue() {
+    Map<String, Object> properties = newHashMap();
+    properties.put("issues", "ABCD");
+    properties.put("actions", "assign");
 
-    when(workflow.doTransition(eq(issue), eq(transition), any(IssueChangeContext.class))).thenReturn(true);
+    when(action.supports(any(Issue.class))).thenReturn(true);
+    when(action.execute(anyMap(), any(IssueBulkChangeService.ActionContext.class))).thenReturn(false);
 
-    IssueBulkChangeQuery issueBulkChangeQuery = IssueBulkChangeQuery.builder().issueKeys(newArrayList(issue.key())).transition(transition).build();
+    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties);
     List<Issue> result = service.execute(issueBulkChangeQuery, userSession);
-    assertThat(result).hasSize(1);
+    assertThat(result).isEmpty();
 
-    verify(workflow).doTransition(eq(issue), eq(transition), any(IssueChangeContext.class));
-    verifyNoMoreInteractions(issueUpdater);
-    verify(issueStorage).save(eq(issue));
-    verifyNoMoreInteractions(issueStorage);
-    verify(issueNotifications).sendChanges(eq(issue), any(IssueChangeContext.class), eq(issueQueryResult));
-    verifyNoMoreInteractions(issueNotifications);
+    verifyZeroInteractions(issueUpdater);
+    verifyZeroInteractions(issueStorage);
+    verifyZeroInteractions(issueNotifications);
   }
 
   @Test
-  public void should_do_bulk_comment(){
-    String comment = "Bulk change comment";
+  public void should_not_execute_bulk_on_unexpected_error() {
+    Map<String, Object> properties = newHashMap();
+    properties.put("issues", "ABCD");
+    properties.put("actions", "assign");
+
+    when(action.supports(any(Issue.class))).thenReturn(true);
+    doThrow(new RuntimeException("Error")).when(action).execute(anyMap(), any(IssueBulkChangeService.ActionContext.class));
 
-    IssueBulkChangeQuery issueBulkChangeQuery = IssueBulkChangeQuery.builder().issueKeys(newArrayList(issue.key())).comment(comment).build();
+    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties);
     List<Issue> result = service.execute(issueBulkChangeQuery, userSession);
-    assertThat(result).hasSize(1);
+    assertThat(result).isEmpty();
 
-    verify(issueUpdater).addComment(eq(issue), eq(comment), any(IssueChangeContext.class));
-    verifyNoMoreInteractions(issueUpdater);
-    verify(issueStorage).save(eq(issue));
-    verifyNoMoreInteractions(issueStorage);
-    verify(issueNotifications).sendChanges(eq(issue), any(IssueChangeContext.class), eq(issueQueryResult));
-    verifyNoMoreInteractions(issueNotifications);
+    verifyZeroInteractions(issueUpdater);
+    verifyZeroInteractions(issueStorage);
+    verifyZeroInteractions(issueNotifications);
   }
 
   @Test
-  public void should_ignore_an_issue_if_an_action_fail(){
-    when(issueQueryResult.issues()).thenReturn(newArrayList((Issue) issue, new DefaultIssue().setKey("EFGH")));
-
-    // Bulk change with 2 actions : severity and comment
-    IssueBulkChangeQuery issueBulkChangeQuery = IssueBulkChangeQuery.builder().issueKeys(newArrayList("ABCD", "EFGH")).severity("MAJOR").comment("Bulk change comment").build();
-
-    // The first call the change severity is ok, the second will fail
-    when(issueUpdater.setManualSeverity(any(DefaultIssue.class), eq("MAJOR"),any(IssueChangeContext.class))).thenReturn(true).thenThrow(new RuntimeException("Cant change severity"));
-
-    List<Issue> result = service.execute(issueBulkChangeQuery, userSession);
-    assertThat(result).hasSize(1);
-    assertThat(result.get(0).key()).isEqualTo("ABCD");
-
-    verify(issueStorage).save(eq(issue));
-    verifyNoMoreInteractions(issueStorage);
-    verify(issueNotifications).sendChanges(eq(issue), any(IssueChangeContext.class), eq(issueQueryResult));
-    verifyNoMoreInteractions(issueNotifications);
+  public void should_fail_if_user_not_loggued() {
+    when(userSession.isLoggedIn()).thenReturn(false);
+
+    Map<String, Object> properties = newHashMap();
+    properties.put("issues", "ABCD");
+    properties.put("actions", "assign");
+    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties);
+    try {
+      service.execute(issueBulkChangeQuery, userSession);
+      fail();
+    } catch (Exception e) {
+      assertThat(e).isInstanceOf(IllegalStateException.class).hasMessage("User is not logged in");
+    }
+    verifyZeroInteractions(issueUpdater);
+    verifyZeroInteractions(issueUpdater);
+    verifyZeroInteractions(issueStorage);
+    verifyZeroInteractions(issueNotifications);
   }
 
 }
diff --git a/sonar-server/src/test/java/org/sonar/server/issue/PlanActionTest.java b/sonar-server/src/test/java/org/sonar/server/issue/PlanActionTest.java
new file mode 100644 (file)
index 0000000..31c6392
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.issue;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.issue.internal.IssueChangeContext;
+import org.sonar.core.issue.DefaultActionPlan;
+import org.sonar.core.issue.IssueUpdater;
+import org.sonar.server.user.UserSession;
+
+import java.util.Map;
+
+import static com.google.common.collect.Maps.newHashMap;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.*;
+
+public class PlanActionTest {
+
+  private PlanAction action;
+
+  private ActionPlanService actionPlanService = mock(ActionPlanService.class);
+
+  @Before
+  public void before(){
+    action = new PlanAction(actionPlanService);
+  }
+
+  @Test
+  public void should_execute(){
+    String planKey = "ABCD";
+    Map<String, Object> properties = newHashMap();
+    properties.put("plan", planKey);
+    DefaultIssue issue = mock(DefaultIssue.class);
+    IssueUpdater issueUpdater = mock(IssueUpdater.class);
+
+    Action.Context context = mock(Action.Context.class);
+    when(context.issueUpdater()).thenReturn(issueUpdater);
+    when(context.issue()).thenReturn(issue);
+
+    action.execute(properties, context);
+    verify(issueUpdater).plan(eq(issue), eq(planKey), any(IssueChangeContext.class));
+  }
+
+  @Test
+  public void should_verify_action_plan_exists(){
+    String planKey = "ABCD";
+    Map<String, Object> properties = newHashMap();
+    properties.put("plan", planKey);
+
+    when(actionPlanService.findByKey(eq(planKey), any(UserSession.class))).thenReturn(new DefaultActionPlan());
+    assertThat(action.verify(properties, mock(UserSession.class))).isTrue();
+
+    when(actionPlanService.findByKey(eq(planKey), any(UserSession.class))).thenReturn(null);
+    try {
+      action.verify(properties, mock(UserSession.class));
+    } catch (Exception e) {
+      assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Unknown action plan: ABCD");
+    }
+  }
+
+  @Test
+  public void should_support_only_unresolved_issues(){
+    assertThat(action.supports(new DefaultIssue().setStatus(Issue.STATUS_OPEN))).isTrue();
+    assertThat(action.supports(new DefaultIssue().setStatus(Issue.STATUS_CLOSED))).isTrue();
+  }
+}
\ No newline at end of file
diff --git a/sonar-server/src/test/java/org/sonar/server/issue/SetSeverityActionTest.java b/sonar-server/src/test/java/org/sonar/server/issue/SetSeverityActionTest.java
new file mode 100644 (file)
index 0000000..82aeb4f
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.issue;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.issue.internal.IssueChangeContext;
+import org.sonar.core.issue.IssueUpdater;
+
+import java.util.Map;
+
+import static com.google.common.collect.Maps.newHashMap;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.*;
+
+public class SetSeverityActionTest {
+
+  private SetSeverityAction action;
+
+  @Before
+  public void before(){
+    action = new SetSeverityAction();
+  }
+
+  @Test
+  public void should_execute(){
+    String severity = "MINOR";
+    Map<String, Object> properties = newHashMap();
+    properties.put("severity", severity);
+    DefaultIssue issue = mock(DefaultIssue.class);
+    IssueUpdater issueUpdater = mock(IssueUpdater.class);
+
+    Action.Context context = mock(Action.Context.class);
+    when(context.issueUpdater()).thenReturn(issueUpdater);
+    when(context.issue()).thenReturn(issue);
+
+    action.execute(properties, context);
+    verify(issueUpdater).setSeverity(eq(issue), eq(severity), any(IssueChangeContext.class));
+  }
+
+  @Test
+  public void should_support_only_unresolved_issues(){
+    assertThat(action.supports(new DefaultIssue().setStatus(Issue.STATUS_OPEN))).isTrue();
+    assertThat(action.supports(new DefaultIssue().setStatus(Issue.STATUS_CLOSED))).isTrue();
+  }
+
+}