Add action for short predefined comments

AddStandardComments allows to add short Its comments for merging,
abandoning, restoring of changes and adding of patch sets.

Change-Id: Id0c699ce353e3ff92b4de178cf9fd4ec2e5936a5
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/ItsHookModule.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/ItsHookModule.java
index 0fcf032..9e18eef 100644
--- a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/ItsHookModule.java
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/ItsHookModule.java
@@ -29,6 +29,7 @@
 import com.googlesource.gerrit.plugins.hooks.workflow.ActionController;
 import com.googlesource.gerrit.plugins.hooks.workflow.Property;
 import com.googlesource.gerrit.plugins.hooks.workflow.Rule;
+import com.googlesource.gerrit.plugins.hooks.workflow.action.AddStandardComment;
 
 public class ItsHookModule extends FactoryModule {
 
@@ -57,5 +58,6 @@
     factory(Property.Factory.class);
     factory(Condition.Factory.class);
     factory(Rule.Factory.class);
+    factory(AddStandardComment.Factory.class);
   }
 }
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionController.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionController.java
index e5c4728..16657fb 100644
--- a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionController.java
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionController.java
@@ -52,7 +52,7 @@
         for (Property property : properties) {
           if ("issue".equals(property.getKey())) {
             String issue = property.getValue();
-            actionExecutor.execute(issue, actions);
+            actionExecutor.execute(issue, actions, properties);
           }
         }
       }
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionExecutor.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionExecutor.java
index 029ef8d..4e99423 100644
--- a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionExecutor.java
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionExecutor.java
@@ -15,12 +15,15 @@
 package com.googlesource.gerrit.plugins.hooks.workflow;
 
 import java.io.IOException;
+import java.util.Set;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.hooks.its.ItsFacade;
+import com.googlesource.gerrit.plugins.hooks.workflow.action.Action;
+import com.googlesource.gerrit.plugins.hooks.workflow.action.AddStandardComment;
 
 /**
  * Executes an {@link ActionRequest}
@@ -30,23 +33,38 @@
       ActionExecutor.class);
 
   private final ItsFacade its;
+  private final AddStandardComment.Factory addStandardCommentFactory;
 
   @Inject
-  public ActionExecutor(ItsFacade its) {
+  public ActionExecutor(ItsFacade its,
+      AddStandardComment.Factory addStandardCommentFactory) {
     this.its = its;
+    this.addStandardCommentFactory = addStandardCommentFactory;
   }
 
-  public void execute(String issue, ActionRequest actionRequest) {
+  public void execute(String issue, ActionRequest actionRequest,
+      Set<Property> properties) {
     try {
-      its.performAction(issue, actionRequest.getUnparsed());
+      String name = actionRequest.getName();
+      Action action = null;
+      if ("add-standard-comment".equals(name)) {
+        action = addStandardCommentFactory.create();
+      }
+
+      if (action == null) {
+        its.performAction(issue, actionRequest.getUnparsed());
+      } else {
+        action.execute(issue, actionRequest, properties);
+      }
     } catch (IOException e) {
       log.error("Error while executing action " + actionRequest, e);
     }
   }
 
-  public void execute(String issue, Iterable<ActionRequest> actions) {
+  public void execute(String issue, Iterable<ActionRequest> actions,
+      Set<Property> properties) {
     for (ActionRequest actionRequest : actions) {
-        execute(issue, actionRequest);
+        execute(issue, actionRequest, properties);
     }
   }
 }
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionRequest.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionRequest.java
index 89e4bde..cb3e5f3 100644
--- a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionRequest.java
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionRequest.java
@@ -27,6 +27,7 @@
  */
 public class ActionRequest {
   private final String unparsed;
+  private final String[] chopped;
 
   public interface Factory {
     ActionRequest create(String specification);
@@ -39,6 +40,21 @@
     } else {
       this.unparsed = specification;
     }
+    this.chopped = unparsed.split(" ");
+  }
+
+  /**
+   * Gets the name of the requested action.
+   *
+   * @return The name of the requested action, if a name has been given.
+   *    "" otherwise.
+   */
+  public String getName() {
+    String ret = "";
+    if (chopped.length > 0) {
+      ret = chopped[0];
+    }
+    return ret;
   }
 
   /**
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/action/Action.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/action/Action.java
new file mode 100644
index 0000000..7e0595c
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/action/Action.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.hooks.workflow.action;
+
+import java.io.IOException;
+import java.util.Set;
+
+import com.googlesource.gerrit.plugins.hooks.workflow.ActionRequest;
+import com.googlesource.gerrit.plugins.hooks.workflow.Property;
+
+/**
+ * Interface for actions on an issue tracking system
+ */
+public interface Action {
+
+  /**
+   * Execute this action.
+   *
+   * @param issue The issue to execute on.
+   * @param actionRequest The request to execute.
+   * @param properties The properties for the execution.
+   */
+  public void execute(String issue, ActionRequest actionRequest,
+      Set<Property> properties) throws IOException;
+}
diff --git a/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/action/AddStandardComment.java b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/action/AddStandardComment.java
new file mode 100644
index 0000000..8cdeef7
--- /dev/null
+++ b/hooks-its/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/action/AddStandardComment.java
@@ -0,0 +1,129 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.hooks.workflow.action;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.hooks.its.ItsFacade;
+import com.googlesource.gerrit.plugins.hooks.workflow.ActionRequest;
+import com.googlesource.gerrit.plugins.hooks.workflow.Property;
+
+/**
+ * Adds a short predefined comments to an issue.
+ *
+ * Comments are added for merging, abandoning, restoring of changes and adding
+ * of patch sets.
+ */
+public class AddStandardComment implements Action {
+  public interface Factory {
+    AddStandardComment create();
+  }
+
+  private final ItsFacade its;
+
+  @Inject
+  public AddStandardComment(ItsFacade its) {
+    this.its = its;
+  }
+
+  private String formatPerson(String prefix, Map<String, String> map) {
+    String ret = Strings.nullToEmpty(map.get(prefix + "-name"));
+    if (ret.isEmpty()) {
+      ret = Strings.nullToEmpty(map.get(prefix + "-username"));
+    }
+    return ret;
+  }
+
+  private String getCommentChangeEvent(String Action, String prefix,
+      Map<String, String> map) {
+    String ret = "";
+    String changeNumber = Strings.nullToEmpty(map.get("change-number"));
+    if (!changeNumber.isEmpty()) {
+      changeNumber += " ";
+    }
+    ret += "Change " + changeNumber + Action;
+    String submitter = formatPerson(prefix, map);
+    if (!submitter.isEmpty()) {
+      ret += " by " + submitter;
+    }
+    String subject = Strings.nullToEmpty(map.get("subject"));
+    if (!subject.isEmpty()) {
+      ret += ":\n" + subject;
+    }
+    String reason = Strings.nullToEmpty(map.get("reason"));
+    if (!reason.isEmpty()) {
+      ret += "\n\nReason:\n" + reason;
+    }
+    String url = Strings.nullToEmpty(map.get("change-url"));
+    if (!url.isEmpty()) {
+      ret += "\n\n" + its.createLinkForWebui(url, url);
+    }
+    return ret;
+  }
+
+  private String getCommentChangeAbandoned(Map<String, String> map) {
+    return getCommentChangeEvent("abandoned", "abandoner", map);
+  }
+
+  private String getCommentChangeMerged(Map<String, String> map) {
+    return getCommentChangeEvent("merged", "submitter", map);
+  }
+
+  private String getCommentChangeRestored(Map<String, String> map) {
+    return getCommentChangeEvent("restored", "restorer", map);
+  }
+
+  private String getCommentPatchSetCreated(Map<String, String> map) {
+    return getCommentChangeEvent("had a related patch set uploaded",
+        "uploader", map);
+  }
+
+  @Override
+  public void execute(String issue, ActionRequest actionRequest,
+      Set<Property> properties) throws IOException {
+    String comment = "";
+    Map<String, String> map = Maps.newHashMap();
+    for (Property property : properties) {
+      String current = property.getValue();
+      if (!Strings.isNullOrEmpty(current))
+      {
+        String key = property.getKey();
+        String old = Strings.nullToEmpty(map.get(key));
+        if (!old.isEmpty()) {
+          old += ", ";
+        }
+        map.put(key, old + current);
+      }
+    }
+    String eventType = map.get("event-type");
+    if ("change-abandoned".equals(eventType)) {
+      comment = getCommentChangeAbandoned(map);
+    } else if ("change-merged".equals(eventType)) {
+      comment = getCommentChangeMerged(map);
+    } else if ("change-restored".equals(eventType)) {
+      comment = getCommentChangeRestored(map);
+    } else if ("patchset-created".equals(eventType)) {
+      comment = getCommentPatchSetCreated(map);
+    }
+    if (!Strings.isNullOrEmpty(comment)) {
+      its.addComment(issue, comment);
+    }
+  }
+}
diff --git a/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionControllerTest.java b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionControllerTest.java
index 926fe8b..d6c9c2d 100644
--- a/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionControllerTest.java
+++ b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionControllerTest.java
@@ -116,7 +116,7 @@
     expect(ruleBase.actionRequestsFor(propertySet)).andReturn(actionRequests)
         .once();
 
-    actionExecutor.execute("testIssue", actionRequests);
+    actionExecutor.execute("testIssue", actionRequests, propertySet);
 
     replayMocks();
 
@@ -167,9 +167,9 @@
     expect(ruleBase.actionRequestsFor(propertySet2)).andReturn(actionRequests2)
         .once();
 
-    actionExecutor.execute("testIssue", actionRequests1);
-    actionExecutor.execute("testIssue", actionRequests2);
-    actionExecutor.execute("testIssue2", actionRequests2);
+    actionExecutor.execute("testIssue", actionRequests1, propertySet1);
+    actionExecutor.execute("testIssue", actionRequests2, propertySet2);
+    actionExecutor.execute("testIssue2", actionRequests2, propertySet2);
 
     replayMocks();
 
diff --git a/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionExecutorTest.java b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionExecutorTest.java
index 5a93b4c..a243812 100644
--- a/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionExecutorTest.java
+++ b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionExecutorTest.java
@@ -17,6 +17,8 @@
 import static org.easymock.EasyMock.expectLastCall;
 
 import java.io.IOException;
+import java.util.Collections;
+import java.util.Set;
 
 import com.google.common.collect.Sets;
 import com.google.gerrit.server.config.FactoryModule;
@@ -24,46 +26,58 @@
 import com.google.inject.Injector;
 import com.googlesource.gerrit.plugins.hooks.its.ItsFacade;
 import com.googlesource.gerrit.plugins.hooks.testutil.LoggingMockingTestCase;
+import com.googlesource.gerrit.plugins.hooks.workflow.action.AddStandardComment;
 
 public class ActionExecutorTest extends LoggingMockingTestCase {
   private Injector injector;
 
   private ItsFacade its;
+  private AddStandardComment.Factory addStandardCommentFactory;
 
   public void testExecuteItem() throws IOException {
     ActionRequest actionRequest = createMock(ActionRequest.class);
+    expect(actionRequest.getName()).andReturn("unparsed");
     expect(actionRequest.getUnparsed()).andReturn("unparsed action 1");
 
+    Set<Property> properties = Collections.emptySet();
+
     its.performAction("4711", "unparsed action 1");
 
     replayMocks();
 
     ActionExecutor actionExecutor = createActionExecutor();
-    actionExecutor.execute("4711", actionRequest);
+    actionExecutor.execute("4711", actionRequest, properties);
   }
 
   public void testExecuteItemException() throws IOException {
     ActionRequest actionRequest = createMock(ActionRequest.class);
+    expect(actionRequest.getName()).andReturn("unparsed");
     expect(actionRequest.getUnparsed()).andReturn("unparsed action 1");
 
+    Set<Property> properties = Collections.emptySet();
+
     its.performAction("4711", "unparsed action 1");
     expectLastCall().andThrow(new IOException("injected exception 1"));
 
     replayMocks();
 
     ActionExecutor actionExecutor = createActionExecutor();
-    actionExecutor.execute("4711", actionRequest);
+    actionExecutor.execute("4711", actionRequest, properties);
 
     assertLogThrowableMessageContains("injected exception 1");
   }
 
   public void testExecuteIterable() throws IOException {
     ActionRequest actionRequest1 = createMock(ActionRequest.class);
+    expect(actionRequest1.getName()).andReturn("unparsed");
     expect(actionRequest1.getUnparsed()).andReturn("unparsed action 1");
 
     ActionRequest actionRequest2 = createMock(ActionRequest.class);
+    expect(actionRequest2.getName()).andReturn("unparsed");
     expect(actionRequest2.getUnparsed()).andReturn("unparsed action 2");
 
+    Set<Property> properties = Collections.emptySet();
+
     its.performAction("4711", "unparsed action 1");
     its.performAction("4711", "unparsed action 2");
 
@@ -71,19 +85,24 @@
 
     ActionExecutor actionExecutor = createActionExecutor();
     actionExecutor.execute("4711", Sets.newHashSet(
-        actionRequest1, actionRequest2));
+        actionRequest1, actionRequest2), properties);
   }
 
   public void testExecuteIterableExceptions() throws IOException {
     ActionRequest actionRequest1 = createMock(ActionRequest.class);
+    expect(actionRequest1.getName()).andReturn("unparsed");
     expect(actionRequest1.getUnparsed()).andReturn("unparsed action 1");
 
     ActionRequest actionRequest2 = createMock(ActionRequest.class);
+    expect(actionRequest2.getName()).andReturn("unparsed");
     expect(actionRequest2.getUnparsed()).andReturn("unparsed action 2");
 
     ActionRequest actionRequest3 = createMock(ActionRequest.class);
+    expect(actionRequest3.getName()).andReturn("unparsed");
     expect(actionRequest3.getUnparsed()).andReturn("unparsed action 3");
 
+    Set<Property> properties = Collections.emptySet();
+
     its.performAction("4711", "unparsed action 1");
     expectLastCall().andThrow(new IOException("injected exception 1"));
     its.performAction("4711", "unparsed action 2");
@@ -94,7 +113,7 @@
 
     ActionExecutor actionExecutor = createActionExecutor();
     actionExecutor.execute("4711", Sets.newHashSet(
-        actionRequest1, actionRequest2, actionRequest3));
+        actionRequest1, actionRequest2, actionRequest3), properties);
 
     assertLogThrowableMessageContains("injected exception 1");
     assertLogThrowableMessageContains("injected exception 3");
@@ -114,6 +133,10 @@
     protected void configure() {
       its = createMock(ItsFacade.class);
       bind(ItsFacade.class).toInstance(its);
+
+      addStandardCommentFactory = createMock(AddStandardComment.Factory.class);
+      bind(AddStandardComment.Factory.class).toInstance(
+          addStandardCommentFactory);
     }
   }
 }
\ No newline at end of file
diff --git a/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionRequestTest.java b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionRequestTest.java
index 2445b98..415a4ec 100644
--- a/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionRequestTest.java
+++ b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionRequestTest.java
@@ -55,6 +55,38 @@
         actionRequest.getUnparsed());
   }
 
+  public void testNameParameterless() throws IOException {
+    replayMocks();
+
+    ActionRequest actionRequest = createActionRequest("action");
+    assertEquals("Unparsed string does not match", "action",
+        actionRequest.getName());
+  }
+
+  public void testNameSingleParameter() throws IOException {
+    replayMocks();
+
+    ActionRequest actionRequest = createActionRequest("action param");
+    assertEquals("Unparsed string does not match", "action",
+        actionRequest.getName());
+  }
+
+  public void testNameMultipleParameters() throws IOException {
+    replayMocks();
+
+    ActionRequest actionRequest = createActionRequest("action param1 param2");
+    assertEquals("Unparsed string does not match", "action",
+        actionRequest.getName());
+  }
+
+  public void testNameNull() throws IOException {
+    replayMocks();
+
+    ActionRequest actionRequest = createActionRequest(null);
+    assertEquals("Unparsed string does not match", "",
+        actionRequest.getName());
+  }
+
   private ActionRequest createActionRequest(String specification) {
     ActionRequest.Factory factory = injector.getInstance(
         ActionRequest.Factory.class);
diff --git a/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/action/AddStandardCommentTest.java b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/action/AddStandardCommentTest.java
new file mode 100644
index 0000000..6b40093
--- /dev/null
+++ b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/action/AddStandardCommentTest.java
@@ -0,0 +1,318 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.googlesource.gerrit.plugins.hooks.workflow.action;
+
+import static org.easymock.EasyMock.expect;
+
+import java.io.IOException;
+import java.util.Set;
+
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.googlesource.gerrit.plugins.hooks.its.ItsFacade;
+import com.googlesource.gerrit.plugins.hooks.testutil.LoggingMockingTestCase;
+import com.googlesource.gerrit.plugins.hooks.workflow.ActionRequest;
+import com.googlesource.gerrit.plugins.hooks.workflow.Property;
+
+public class AddStandardCommentTest extends LoggingMockingTestCase {
+  private Injector injector;
+
+  private ItsFacade its;
+
+  public void testChangeMergedPlain() throws IOException {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+
+    Set<Property> properties = Sets.newHashSet();
+
+    Property propertyEventType = createMock(Property.class);
+    expect(propertyEventType.getKey()).andReturn("event-type").anyTimes();
+    expect(propertyEventType.getValue()).andReturn("change-merged").anyTimes();
+    properties.add(propertyEventType);
+
+    its.addComment("42", "Change merged");
+    replayMocks();
+
+    Action action = injector.getInstance(AddStandardComment.class);
+    action.execute("42", actionRequest, properties);
+  }
+
+  public void testChangeMergedFull() throws IOException {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+
+    Set<Property> properties = Sets.newHashSet();
+
+    Property propertyEventType = createMock(Property.class);
+    expect(propertyEventType.getKey()).andReturn("event-type").anyTimes();
+    expect(propertyEventType.getValue()).andReturn("change-merged").anyTimes();
+    properties.add(propertyEventType);
+
+    Property propertySubject = createMock(Property.class);
+    expect(propertySubject.getKey()).andReturn("subject").anyTimes();
+    expect(propertySubject.getValue()).andReturn("Test-Change-Subject").anyTimes();
+    properties.add(propertySubject);
+
+    Property propertyChangeNumber = createMock(Property.class);
+    expect(propertyChangeNumber.getKey()).andReturn("change-number")
+        .anyTimes();
+    expect(propertyChangeNumber.getValue()).andReturn("4711").anyTimes();
+    properties.add(propertyChangeNumber);
+
+    Property propertySubmitterName = createMock(Property.class);
+    expect(propertySubmitterName.getKey()).andReturn("submitter-name")
+        .anyTimes();
+    expect(propertySubmitterName.getValue()).andReturn("John Doe").anyTimes();
+    properties.add(propertySubmitterName);
+
+    Property propertyChangeUrl= createMock(Property.class);
+    expect(propertyChangeUrl.getKey()).andReturn("change-url").anyTimes();
+    expect(propertyChangeUrl.getValue()).andReturn("http://example.org/change")
+        .anyTimes();
+    properties.add(propertyChangeUrl);
+
+    expect(its.createLinkForWebui("http://example.org/change",
+        "http://example.org/change")).andReturn("HtTp://ExAmPlE.OrG/ChAnGe");
+
+    its.addComment("176", "Change 4711 merged by John Doe:\n" +
+        "Test-Change-Subject\n" +
+        "\n" +
+        "HtTp://ExAmPlE.OrG/ChAnGe");
+    replayMocks();
+
+    Action action = injector.getInstance(AddStandardComment.class);
+    action.execute("176", actionRequest, properties);
+  }
+
+  public void testChangeAbandonedPlain() throws IOException {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+
+    Set<Property> properties = Sets.newHashSet();
+
+    Property propertyEventType = createMock(Property.class);
+    expect(propertyEventType.getKey()).andReturn("event-type").anyTimes();
+    expect(propertyEventType.getValue()).andReturn("change-abandoned").anyTimes();
+    properties.add(propertyEventType);
+
+    its.addComment("42", "Change abandoned");
+    replayMocks();
+
+    Action action = injector.getInstance(AddStandardComment.class);
+    action.execute("42", actionRequest, properties);
+  }
+
+  public void testChangeAbandonedFull() throws IOException {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+
+    Set<Property> properties = Sets.newHashSet();
+
+    Property propertyEventType = createMock(Property.class);
+    expect(propertyEventType.getKey()).andReturn("event-type").anyTimes();
+    expect(propertyEventType.getValue()).andReturn("change-abandoned").anyTimes();
+    properties.add(propertyEventType);
+
+    Property propertyReason = createMock(Property.class);
+    expect(propertyReason.getKey()).andReturn("reason").anyTimes();
+    expect(propertyReason.getValue()).andReturn("Test-Reason").anyTimes();
+    properties.add(propertyReason);
+
+    Property propertySubject = createMock(Property.class);
+    expect(propertySubject.getKey()).andReturn("subject").anyTimes();
+    expect(propertySubject.getValue()).andReturn("Test-Change-Subject").anyTimes();
+    properties.add(propertySubject);
+
+    Property propertyChangeNumber = createMock(Property.class);
+    expect(propertyChangeNumber.getKey()).andReturn("change-number")
+        .anyTimes();
+    expect(propertyChangeNumber.getValue()).andReturn("4711").anyTimes();
+    properties.add(propertyChangeNumber);
+
+    Property propertySubmitterName = createMock(Property.class);
+    expect(propertySubmitterName.getKey()).andReturn("abandoner-name")
+        .anyTimes();
+    expect(propertySubmitterName.getValue()).andReturn("John Doe").anyTimes();
+    properties.add(propertySubmitterName);
+
+    Property propertyChangeUrl= createMock(Property.class);
+    expect(propertyChangeUrl.getKey()).andReturn("change-url").anyTimes();
+    expect(propertyChangeUrl.getValue()).andReturn("http://example.org/change")
+        .anyTimes();
+    properties.add(propertyChangeUrl);
+
+    expect(its.createLinkForWebui("http://example.org/change",
+        "http://example.org/change")).andReturn("HtTp://ExAmPlE.OrG/ChAnGe");
+
+    its.addComment("176", "Change 4711 abandoned by John Doe:\n" +
+        "Test-Change-Subject\n" +
+        "\n" +
+        "Reason:\n" +
+        "Test-Reason\n" +
+        "\n" +
+        "HtTp://ExAmPlE.OrG/ChAnGe");
+    replayMocks();
+
+    Action action = injector.getInstance(AddStandardComment.class);
+    action.execute("176", actionRequest, properties);
+  }
+
+  public void testChangeRestoredPlain() throws IOException {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+
+    Set<Property> properties = Sets.newHashSet();
+
+    Property propertyEventType = createMock(Property.class);
+    expect(propertyEventType.getKey()).andReturn("event-type").anyTimes();
+    expect(propertyEventType.getValue()).andReturn("change-restored").anyTimes();
+    properties.add(propertyEventType);
+
+    its.addComment("42", "Change restored");
+    replayMocks();
+
+    Action action = injector.getInstance(AddStandardComment.class);
+    action.execute("42", actionRequest, properties);
+  }
+
+  public void testChangeRestoredFull() throws IOException {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+
+    Set<Property> properties = Sets.newHashSet();
+
+    Property propertyEventType = createMock(Property.class);
+    expect(propertyEventType.getKey()).andReturn("event-type").anyTimes();
+    expect(propertyEventType.getValue()).andReturn("change-restored").anyTimes();
+    properties.add(propertyEventType);
+
+    Property propertyReason = createMock(Property.class);
+    expect(propertyReason.getKey()).andReturn("reason").anyTimes();
+    expect(propertyReason.getValue()).andReturn("Test-Reason").anyTimes();
+    properties.add(propertyReason);
+
+    Property propertySubject = createMock(Property.class);
+    expect(propertySubject.getKey()).andReturn("subject").anyTimes();
+    expect(propertySubject.getValue()).andReturn("Test-Change-Subject").anyTimes();
+    properties.add(propertySubject);
+
+    Property propertyChangeNumber = createMock(Property.class);
+    expect(propertyChangeNumber.getKey()).andReturn("change-number")
+        .anyTimes();
+    expect(propertyChangeNumber.getValue()).andReturn("4711").anyTimes();
+    properties.add(propertyChangeNumber);
+
+    Property propertySubmitterName = createMock(Property.class);
+    expect(propertySubmitterName.getKey()).andReturn("restorer-name")
+        .anyTimes();
+    expect(propertySubmitterName.getValue()).andReturn("John Doe").anyTimes();
+    properties.add(propertySubmitterName);
+
+    Property propertyChangeUrl= createMock(Property.class);
+    expect(propertyChangeUrl.getKey()).andReturn("change-url").anyTimes();
+    expect(propertyChangeUrl.getValue()).andReturn("http://example.org/change")
+        .anyTimes();
+    properties.add(propertyChangeUrl);
+
+    expect(its.createLinkForWebui("http://example.org/change",
+        "http://example.org/change")).andReturn("HtTp://ExAmPlE.OrG/ChAnGe");
+
+    its.addComment("176", "Change 4711 restored by John Doe:\n" +
+        "Test-Change-Subject\n" +
+        "\n" +
+        "Reason:\n" +
+        "Test-Reason\n" +
+        "\n" +
+        "HtTp://ExAmPlE.OrG/ChAnGe");
+    replayMocks();
+
+    Action action = injector.getInstance(AddStandardComment.class);
+    action.execute("176", actionRequest, properties);
+  }
+
+  public void testPatchSetCreatedPlain() throws IOException {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+
+    Set<Property> properties = Sets.newHashSet();
+
+    Property propertyEventType = createMock(Property.class);
+    expect(propertyEventType.getKey()).andReturn("event-type").anyTimes();
+    expect(propertyEventType.getValue()).andReturn("patchset-created").anyTimes();
+    properties.add(propertyEventType);
+
+    its.addComment("42", "Change had a related patch set uploaded");
+    replayMocks();
+
+    Action action = injector.getInstance(AddStandardComment.class);
+    action.execute("42", actionRequest, properties);
+  }
+
+  public void testPatchSetCreatedFull() throws IOException {
+    ActionRequest actionRequest = createMock(ActionRequest.class);
+
+    Set<Property> properties = Sets.newHashSet();
+
+    Property propertyEventType = createMock(Property.class);
+    expect(propertyEventType.getKey()).andReturn("event-type").anyTimes();
+    expect(propertyEventType.getValue()).andReturn("patchset-created").anyTimes();
+    properties.add(propertyEventType);
+
+    Property propertySubject = createMock(Property.class);
+    expect(propertySubject.getKey()).andReturn("subject").anyTimes();
+    expect(propertySubject.getValue()).andReturn("Test-Change-Subject").anyTimes();
+    properties.add(propertySubject);
+
+    Property propertyChangeNumber = createMock(Property.class);
+    expect(propertyChangeNumber.getKey()).andReturn("change-number")
+        .anyTimes();
+    expect(propertyChangeNumber.getValue()).andReturn("4711").anyTimes();
+    properties.add(propertyChangeNumber);
+
+    Property propertySubmitterName = createMock(Property.class);
+    expect(propertySubmitterName.getKey()).andReturn("uploader-name")
+        .anyTimes();
+    expect(propertySubmitterName.getValue()).andReturn("John Doe").anyTimes();
+    properties.add(propertySubmitterName);
+
+    Property propertyChangeUrl= createMock(Property.class);
+    expect(propertyChangeUrl.getKey()).andReturn("change-url").anyTimes();
+    expect(propertyChangeUrl.getValue()).andReturn("http://example.org/change")
+        .anyTimes();
+    properties.add(propertyChangeUrl);
+
+    expect(its.createLinkForWebui("http://example.org/change",
+        "http://example.org/change")).andReturn("HtTp://ExAmPlE.OrG/ChAnGe");
+
+    its.addComment("176", "Change 4711 had a related patch set uploaded by " +
+        "John Doe:\n" +
+        "Test-Change-Subject\n" +
+        "\n" +
+        "HtTp://ExAmPlE.OrG/ChAnGe");
+    replayMocks();
+
+    Action action = injector.getInstance(AddStandardComment.class);
+    action.execute("176", actionRequest, properties);
+  }
+
+  public void setUp() throws Exception {
+    super.setUp();
+
+    injector = Guice.createInjector(new TestModule());
+  }
+
+  private class TestModule extends FactoryModule {
+    @Override
+    protected void configure() {
+      its = createMock(ItsFacade.class);
+      bind(ItsFacade.class).toInstance(its);
+    }
+  }
+}
\ No newline at end of file