Merge "Allow ITS comments for a change's patch sets that first associate an issue"
diff --git a/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/util/IssueExtractorTest.java b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/util/IssueExtractorTest.java
new file mode 100644
index 0000000..f1290ea
--- /dev/null
+++ b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/util/IssueExtractorTest.java
@@ -0,0 +1,192 @@
+// 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.util;
+
+import static org.easymock.EasyMock.expect;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.jgit.lib.Config;
+
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.googlesource.gerrit.plugins.hooks.its.ItsName;
+import com.googlesource.gerrit.plugins.hooks.testutil.LoggingMockingTestCase;
+
+public class IssueExtractorTest extends LoggingMockingTestCase {
+  private Injector injector;
+  private Config serverConfig;
+
+  public void testPatternNullMatch() {
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+        .andReturn(null).atLeastOnce();
+
+    replayMocks();
+
+    assertNull("Pattern for null match is not null",
+        issueExtractor.getPattern());
+  }
+
+  public void testPattern() {
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+        .andReturn("TestPattern").atLeastOnce();
+
+    replayMocks();
+
+    assertEquals("Expected and generated pattern are not equal",
+        "TestPattern", issueExtractor.getPattern().pattern());
+  }
+
+  public void testIssueIdsNullPattern() {
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+        .andReturn(null).atLeastOnce();
+
+    replayMocks();
+
+    String ret[] = issueExtractor.getIssueIds("Test");
+    assertEquals("Number of found ids do not match", 0, ret.length);
+  }
+
+  public void testIssueIdsNoMatch() {
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+        .andReturn("bug#(\\d+)").atLeastOnce();
+
+    replayMocks();
+
+    String ret[] = issueExtractor.getIssueIds("Test");
+    assertEquals("Number of found ids do not match", 0, ret.length);
+
+    assertLogMessageContains("Matching");
+  }
+
+  public void testIssueIdsFullMatch() {
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+        .andReturn("bug#(\\d+)").atLeastOnce();
+
+    replayMocks();
+
+    String ret[] = issueExtractor.getIssueIds("bug#4711");
+    assertEquals("Number of found ids do not match", 1, ret.length);
+    assertEquals("First found issue id do not match", "4711", ret[0]);
+
+    assertLogMessageContains("Matching");
+  }
+
+  public void testIssueIdsMatch() {
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+        .andReturn("bug#(\\d+)").atLeastOnce();
+
+    replayMocks();
+
+    String ret[] = issueExtractor.getIssueIds("Foo bug#4711 bar");
+    assertEquals("Number of found ids do not match", 1, ret.length);
+    assertEquals("Found issue id does not match", "4711", ret[0]);
+
+    assertLogMessageContains("Matching");
+  }
+
+  public void testIssueIdsGrouplessMatch() {
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+        .andReturn("bug#\\d+").atLeastOnce();
+
+    replayMocks();
+
+    String ret[] = issueExtractor.getIssueIds("Foo bug#4711 bar");
+    assertEquals("Number of found ids do not match", 1, ret.length);
+    assertEquals("Found issue id does not match", "bug#4711", ret[0]);
+
+    assertLogMessageContains("Matching");
+  }
+
+  public void testIssueIdsMultiGroupMatch() {
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+        .andReturn("bug#(\\d)(\\d+)").atLeastOnce();
+
+    replayMocks();
+
+    String ret[] = issueExtractor.getIssueIds("Foo bug#4711 bar");
+    assertEquals("Number of found ids do not match", 1, ret.length);
+    assertEquals("Found issue id does not match", "4", ret[0]);
+
+    assertLogMessageContains("Matching");
+  }
+
+  public void testIssueIdsMulipleMatches() {
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+        .andReturn("bug#(\\d+)").atLeastOnce();
+
+    replayMocks();
+
+    String ret[] = issueExtractor.getIssueIds("Foo bug#4711 bug#42 bar bug#123");
+    assertEquals("Number of found ids do not match", 3, ret.length);
+    List<String> retList = Arrays.asList(ret);
+    assertTrue("4711 not among the extracted ids", retList.contains("4711"));
+    assertTrue("42 not among the extracted ids", retList.contains("42"));
+    assertTrue("123 not among the extracted ids", retList.contains("123"));
+
+    assertLogMessageContains("Matching");
+  }
+
+  public void testIssueIdsMulipleMatchesWithDuplicates() {
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+        .andReturn("bug#(\\d+)").atLeastOnce();
+
+    replayMocks();
+
+    String ret[] = issueExtractor.getIssueIds("Foo bug#4711 bug#42 bar\n" +
+        "bug#123 baz bug#42");
+    assertEquals("Number of found ids do not match", 3, ret.length);
+    List<String> retList = Arrays.asList(ret);
+    assertTrue("4711 not among the extracted ids", retList.contains("4711"));
+    assertTrue("42 not among the extracted ids", retList.contains("42"));
+    assertTrue("123 not among the extracted ids", retList.contains("123"));
+
+    assertLogMessageContains("Matching");
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+
+    injector = Guice.createInjector(new TestModule());
+  }
+
+  private class TestModule extends FactoryModule {
+    @Override
+    protected void configure() {
+      bind(String.class).annotatedWith(ItsName.class)
+          .toInstance("ItsTestName");
+
+      serverConfig = createMock(Config.class);
+      bind(Config.class).annotatedWith(GerritServerConfig.class)
+          .toInstance(serverConfig);
+    }
+  }
+}
\ No newline at end of file
diff --git a/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/validation/ItsValidateCommentTest.java b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/validation/ItsValidateCommentTest.java
new file mode 100644
index 0000000..2421593
--- /dev/null
+++ b/hooks-its/src/test/java/com/googlesource/gerrit/plugins/hooks/validation/ItsValidateCommentTest.java
@@ -0,0 +1,516 @@
+// 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.validation;
+
+import static org.easymock.EasyMock.expect;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+import org.junit.runner.RunWith;
+
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+import com.googlesource.gerrit.plugins.hooks.its.ItsFacade;
+import com.googlesource.gerrit.plugins.hooks.its.ItsName;
+import com.googlesource.gerrit.plugins.hooks.testutil.LoggingMockingTestCase;
+import com.googlesource.gerrit.plugins.hooks.util.IssueExtractor;
+import com.googlesource.gerrit.plugins.hooks.validation.ItsAssociationPolicy;
+import com.googlesource.gerrit.plugins.hooks.validation.ItsValidateComment;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({RevCommit.class})
+public class ItsValidateCommentTest extends LoggingMockingTestCase {
+  private Injector injector;
+  private Config serverConfig;
+  private IssueExtractor issueExtractor;
+  private ItsFacade itsFacade;
+
+  public void testOptional() throws CommitValidationException {
+    List<CommitValidationMessage> ret;
+    ItsValidateComment ivc = injector.getInstance(ItsValidateComment.class);
+    ReceiveCommand command = createMock(ReceiveCommand.class);
+    RevCommit commit = createMock(RevCommit.class);
+    CommitReceivedEvent event = new CommitReceivedEvent(command, null, null,
+        commit, null);
+    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
+        ItsAssociationPolicy.OPTIONAL)).andReturn(
+            ItsAssociationPolicy.OPTIONAL).atLeastOnce();
+    replayMocks();
+
+    ret = ivc.onCommitReceived(event);
+
+    assertEmptyList(ret);
+  }
+
+  public void testSuggestedNonMatching() throws CommitValidationException {
+    List<CommitValidationMessage> ret;
+    ItsValidateComment ivc = injector.getInstance(ItsValidateComment.class);
+    ReceiveCommand command = createMock(ReceiveCommand.class);
+    RevCommit commit = createMock(RevCommit.class);
+    CommitReceivedEvent event = new CommitReceivedEvent(command, null, null,
+        commit, null);
+    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
+        ItsAssociationPolicy.OPTIONAL)).andReturn(
+            ItsAssociationPolicy.SUGGESTED).atLeastOnce();
+    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
+        .andReturn("TestPattern").anyTimes();
+    expect(commit.getFullMessage()).andReturn("TestMessage").atLeastOnce();
+    expect(commit.getId()).andReturn(commit).anyTimes();
+    expect(commit.getName()).andReturn("TestCommit").anyTimes();
+    expect(issueExtractor.getIssueIds("TestMessage")).andReturn(
+        new String[] {}).atLeastOnce();
+    expect(itsFacade.name()).andReturn("TestFacade").anyTimes();
+
+    replayMocks();
+
+    ret = ivc.onCommitReceived(event);
+
+    assertEquals("Size of returned CommitValidationMessages does not match",
+        1, ret.size());
+    assertTrue("First CommitValidationMessages does not contain 'Missing " +
+        "issue'",ret.get(0).getMessage().contains("Missing issue"));
+  }
+
+  public void testMandatoryNonMatching() throws CommitValidationException {
+    ItsValidateComment ivc = injector.getInstance(ItsValidateComment.class);
+    ReceiveCommand command = createMock(ReceiveCommand.class);
+    RevCommit commit = createMock(RevCommit.class);
+    CommitReceivedEvent event = new CommitReceivedEvent(command, null, null,
+        commit, null);
+    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
+        ItsAssociationPolicy.OPTIONAL)).andReturn(
+            ItsAssociationPolicy.MANDATORY).atLeastOnce();
+    expect(commit.getFullMessage()).andReturn("TestMessage").atLeastOnce();
+    expect(commit.getId()).andReturn(commit).anyTimes();
+    expect(commit.getName()).andReturn("TestCommit").anyTimes();
+    expect(issueExtractor.getIssueIds("TestMessage")).andReturn(
+        new String[] {}).atLeastOnce();
+    expect(itsFacade.name()).andReturn("TestFacade").anyTimes();
+
+    replayMocks();
+
+    try {
+      ivc.onCommitReceived(event);
+      fail("onCommitReceived did not throw any exception");
+    } catch (CommitValidationException e) {
+      assertTrue("Message of thrown CommitValidationException does not "
+          + "contain 'Missing issue'",
+          e.getMessage().contains("Missing issue"));
+    }
+  }
+
+  public void testSuggestedMatchingSingleExisting()
+      throws CommitValidationException, IOException {
+    List<CommitValidationMessage> ret;
+    ItsValidateComment ivc = injector.getInstance(ItsValidateComment.class);
+    ReceiveCommand command = createMock(ReceiveCommand.class);
+    RevCommit commit = createMock(RevCommit.class);
+    CommitReceivedEvent event = new CommitReceivedEvent(command, null, null,
+        commit, null);
+    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
+        ItsAssociationPolicy.OPTIONAL)).andReturn(
+            ItsAssociationPolicy.SUGGESTED).atLeastOnce();
+    expect(commit.getFullMessage()).andReturn("bug#4711").atLeastOnce();
+    expect(commit.getId()).andReturn(commit).anyTimes();
+    expect(commit.getName()).andReturn("TestCommit").anyTimes();
+    expect(issueExtractor.getIssueIds("bug#4711")).andReturn(
+        new String[] {"4711"}).atLeastOnce();
+    expect(itsFacade.name()).andReturn("TestFacade").anyTimes();
+    expect(itsFacade.exists("4711")).andReturn(true).atLeastOnce();
+
+    replayMocks();
+
+    ret = ivc.onCommitReceived(event);
+
+    assertEmptyList(ret);
+  }
+
+  public void testMandatoryMatchingSingleExisting()
+      throws CommitValidationException, IOException {
+    List<CommitValidationMessage> ret;
+    ItsValidateComment ivc = injector.getInstance(ItsValidateComment.class);
+    ReceiveCommand command = createMock(ReceiveCommand.class);
+    RevCommit commit = createMock(RevCommit.class);
+    CommitReceivedEvent event = new CommitReceivedEvent(command, null, null,
+        commit, null);
+    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
+        ItsAssociationPolicy.OPTIONAL)).andReturn(
+            ItsAssociationPolicy.MANDATORY).atLeastOnce();
+    expect(commit.getFullMessage()).andReturn("bug#4711").atLeastOnce();
+    expect(commit.getId()).andReturn(commit).anyTimes();
+    expect(commit.getName()).andReturn("TestCommit").anyTimes();
+    expect(issueExtractor.getIssueIds("bug#4711")).andReturn(
+        new String[] {"4711"}).atLeastOnce();
+    expect(itsFacade.name()).andReturn("TestFacade").anyTimes();
+    expect(itsFacade.exists("4711")).andReturn(true).atLeastOnce();
+
+    replayMocks();
+
+    ret = ivc.onCommitReceived(event);
+
+    assertEmptyList(ret);
+  }
+
+  public void testSuggestedMatchingSingleNonExisting()
+      throws CommitValidationException, IOException {
+    List<CommitValidationMessage> ret;
+    ItsValidateComment ivc = injector.getInstance(ItsValidateComment.class);
+    ReceiveCommand command = createMock(ReceiveCommand.class);
+    RevCommit commit = createMock(RevCommit.class);
+    CommitReceivedEvent event = new CommitReceivedEvent(command, null, null,
+        commit, null);
+    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
+        ItsAssociationPolicy.OPTIONAL)).andReturn(
+            ItsAssociationPolicy.SUGGESTED).atLeastOnce();
+    expect(commit.getFullMessage()).andReturn("bug#4711").atLeastOnce();
+    expect(commit.getId()).andReturn(commit).anyTimes();
+    expect(commit.getName()).andReturn("TestCommit").anyTimes();
+    expect(issueExtractor.getIssueIds("bug#4711")).andReturn(
+        new String[] {"4711"}).atLeastOnce();
+    expect(itsFacade.name()).andReturn("TestFacade").anyTimes();
+    expect(itsFacade.exists("4711")).andReturn(false).atLeastOnce();
+
+    replayMocks();
+
+    ret = ivc.onCommitReceived(event);
+
+    assertEquals("Size of returned CommitValidationMessages does not match",
+        1, ret.size());
+    assertTrue("First CommitValidationMessages does not contain " +
+        "'Non-existing'",ret.get(0).getMessage().contains("Non-existing"));
+    assertTrue("First CommitValidationMessages does not contain '4711'",
+        ret.get(0).getMessage().contains("4711"));
+  }
+
+  public void testMandatoryMatchingSingleNonExisting()
+      throws CommitValidationException, IOException {
+    ItsValidateComment ivc = injector.getInstance(ItsValidateComment.class);
+    ReceiveCommand command = createMock(ReceiveCommand.class);
+    RevCommit commit = createMock(RevCommit.class);
+    CommitReceivedEvent event = new CommitReceivedEvent(command, null, null,
+        commit, null);
+    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
+        ItsAssociationPolicy.OPTIONAL)).andReturn(
+            ItsAssociationPolicy.MANDATORY).atLeastOnce();
+    expect(commit.getFullMessage()).andReturn("bug#4711").atLeastOnce();
+    expect(commit.getId()).andReturn(commit).anyTimes();
+    expect(commit.getName()).andReturn("TestCommit").anyTimes();
+    expect(issueExtractor.getIssueIds("bug#4711")).andReturn(
+        new String[] {"4711"}).atLeastOnce();
+    expect(itsFacade.name()).andReturn("TestFacade").anyTimes();
+    expect(itsFacade.exists("4711")).andReturn(false).atLeastOnce();
+
+    replayMocks();
+
+    try {
+      ivc.onCommitReceived(event);
+      fail("onCommitReceived did not throw any exception");
+    } catch (CommitValidationException e) {
+      assertTrue("Message of thrown CommitValidationException does not "
+          + "contain 'Non-existing'", e.getMessage().contains("Non-existing"));
+    }
+  }
+
+  public void testSuggestedMatchingMultiple()
+      throws CommitValidationException, IOException {
+    List<CommitValidationMessage> ret;
+    ItsValidateComment ivc = injector.getInstance(ItsValidateComment.class);
+    ReceiveCommand command = createMock(ReceiveCommand.class);
+    RevCommit commit = createMock(RevCommit.class);
+    CommitReceivedEvent event = new CommitReceivedEvent(command, null, null,
+        commit, null);
+    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
+        ItsAssociationPolicy.OPTIONAL)).andReturn(
+            ItsAssociationPolicy.SUGGESTED).atLeastOnce();
+    expect(commit.getFullMessage()).andReturn("bug#4711, bug#42")
+        .atLeastOnce();
+    expect(commit.getId()).andReturn(commit).anyTimes();
+    expect(commit.getName()).andReturn("TestCommit").anyTimes();
+    expect(issueExtractor.getIssueIds("bug#4711, bug#42")).andReturn(
+        new String[] {"4711", "42"}).atLeastOnce();
+    expect(itsFacade.name()).andReturn("TestFacade").anyTimes();
+    expect(itsFacade.exists("4711")).andReturn(true).atLeastOnce();
+    expect(itsFacade.exists("42")).andReturn(true).atLeastOnce();
+
+    replayMocks();
+
+    ret = ivc.onCommitReceived(event);
+
+    assertEmptyList(ret);
+  }
+
+  public void testMandatoryMatchingMultiple()
+      throws CommitValidationException, IOException {
+    List<CommitValidationMessage> ret;
+    ItsValidateComment ivc = injector.getInstance(ItsValidateComment.class);
+    ReceiveCommand command = createMock(ReceiveCommand.class);
+    RevCommit commit = createMock(RevCommit.class);
+    CommitReceivedEvent event = new CommitReceivedEvent(command, null, null,
+        commit, null);
+    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
+        ItsAssociationPolicy.OPTIONAL)).andReturn(
+            ItsAssociationPolicy.MANDATORY).atLeastOnce();
+    expect(commit.getFullMessage()).andReturn("bug#4711, bug#42")
+        .atLeastOnce();
+    expect(commit.getId()).andReturn(commit).anyTimes();
+    expect(commit.getName()).andReturn("TestCommit").anyTimes();
+    expect(issueExtractor.getIssueIds("bug#4711, bug#42")).andReturn(
+        new String[] {"4711", "42"}).atLeastOnce();
+    expect(itsFacade.name()).andReturn("TestFacade").anyTimes();
+    expect(itsFacade.exists("4711")).andReturn(true).atLeastOnce();
+    expect(itsFacade.exists("42")).andReturn(true).atLeastOnce();
+
+    replayMocks();
+
+    ret = ivc.onCommitReceived(event);
+
+    assertEmptyList(ret);
+  }
+
+  public void testSuggestedMatchingMultipleOneNonExsting()
+      throws CommitValidationException, IOException {
+    List<CommitValidationMessage> ret;
+    ItsValidateComment ivc = injector.getInstance(ItsValidateComment.class);
+    ReceiveCommand command = createMock(ReceiveCommand.class);
+    RevCommit commit = createMock(RevCommit.class);
+    CommitReceivedEvent event = new CommitReceivedEvent(command, null, null,
+        commit, null);
+    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
+        ItsAssociationPolicy.OPTIONAL)).andReturn(
+            ItsAssociationPolicy.SUGGESTED).atLeastOnce();
+    expect(commit.getFullMessage()).andReturn("bug#4711, bug#42")
+        .atLeastOnce();
+    expect(commit.getId()).andReturn(commit).anyTimes();
+    expect(commit.getName()).andReturn("TestCommit").anyTimes();
+    expect(issueExtractor.getIssueIds("bug#4711, bug#42")).andReturn(
+        new String[] {"4711", "42"}).atLeastOnce();
+    expect(itsFacade.name()).andReturn("TestFacade").anyTimes();
+    expect(itsFacade.exists("4711")).andReturn(false).atLeastOnce();
+    expect(itsFacade.exists("42")).andReturn(true).atLeastOnce();
+
+    replayMocks();
+
+    ret = ivc.onCommitReceived(event);
+
+    assertEquals("Size of returned CommitValidationMessages does not match",
+        1, ret.size());
+    assertTrue("First CommitValidationMessages does not contain " +
+        "'Non-existing'",ret.get(0).getMessage().contains("Non-existing"));
+    assertTrue("First CommitValidationMessages does not contain '4711'",
+        ret.get(0).getMessage().contains("4711"));
+    assertFalse("First CommitValidationMessages contains '42', although " +
+        "that bug exists", ret.get(0).getMessage().contains("42"));
+  }
+
+  public void testMandatoryMatchingMultipleOneNonExsting()
+      throws IOException {
+    ItsValidateComment ivc = injector.getInstance(ItsValidateComment.class);
+    ReceiveCommand command = createMock(ReceiveCommand.class);
+    RevCommit commit = createMock(RevCommit.class);
+    CommitReceivedEvent event = new CommitReceivedEvent(command, null, null,
+        commit, null);
+    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
+        ItsAssociationPolicy.OPTIONAL)).andReturn(
+            ItsAssociationPolicy.MANDATORY).atLeastOnce();
+    expect(commit.getFullMessage()).andReturn("bug#4711, bug#42")
+        .atLeastOnce();
+    expect(commit.getId()).andReturn(commit).anyTimes();
+    expect(commit.getName()).andReturn("TestCommit").anyTimes();
+    expect(issueExtractor.getIssueIds("bug#4711, bug#42")).andReturn(
+        new String[] {"4711", "42"}).atLeastOnce();
+    expect(itsFacade.name()).andReturn("TestFacade").anyTimes();
+    expect(itsFacade.exists("4711")).andReturn(false).atLeastOnce();
+    expect(itsFacade.exists("42")).andReturn(true).atLeastOnce();
+
+    replayMocks();
+
+    try {
+      ivc.onCommitReceived(event);
+      fail("onCommitReceived did not throw any exception");
+    } catch (CommitValidationException e) {
+      assertTrue("Message of thrown CommitValidationException does not "
+          + "contain 'Non-existing'", e.getMessage().contains("Non-existing"));
+    }
+  }
+
+  public void testSuggestedMatchingMultipleSomeNonExsting()
+      throws CommitValidationException, IOException {
+    List<CommitValidationMessage> ret;
+    ItsValidateComment ivc = injector.getInstance(ItsValidateComment.class);
+    ReceiveCommand command = createMock(ReceiveCommand.class);
+    RevCommit commit = createMock(RevCommit.class);
+    CommitReceivedEvent event = new CommitReceivedEvent(command, null, null,
+        commit, null);
+    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
+        ItsAssociationPolicy.OPTIONAL)).andReturn(
+            ItsAssociationPolicy.SUGGESTED).atLeastOnce();
+    expect(commit.getFullMessage()).andReturn("bug#4711, bug#42")
+        .atLeastOnce();
+    expect(commit.getId()).andReturn(commit).anyTimes();
+    expect(commit.getName()).andReturn("TestCommit").anyTimes();
+    expect(issueExtractor.getIssueIds("bug#4711, bug#42")).andReturn(
+        new String[] {"4711", "42"}).atLeastOnce();
+    expect(itsFacade.name()).andReturn("TestFacade").anyTimes();
+    expect(itsFacade.exists("4711")).andReturn(false).atLeastOnce();
+    expect(itsFacade.exists("42")).andReturn(false).atLeastOnce();
+
+    replayMocks();
+
+    ret = ivc.onCommitReceived(event);
+
+    assertEquals("Size of returned CommitValidationMessages does not match",
+        1, ret.size());
+    assertTrue("First CommitValidationMessages does not contain " +
+        "'Non-existing'",ret.get(0).getMessage().contains("Non-existing"));
+    assertTrue("First CommitValidationMessages does not contain '4711'",
+        ret.get(0).getMessage().contains("4711"));
+    assertTrue("First CommitValidationMessages does not contain '42'",
+        ret.get(0).getMessage().contains("42"));
+  }
+
+  public void testMandatoryMatchingMultipleSomeNonExsting()
+      throws IOException {
+    ItsValidateComment ivc = injector.getInstance(ItsValidateComment.class);
+    ReceiveCommand command = createMock(ReceiveCommand.class);
+    RevCommit commit = createMock(RevCommit.class);
+    CommitReceivedEvent event = new CommitReceivedEvent(command, null, null,
+        commit, null);
+    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
+        ItsAssociationPolicy.OPTIONAL)).andReturn(
+            ItsAssociationPolicy.MANDATORY).atLeastOnce();
+    expect(commit.getFullMessage()).andReturn("bug#4711, bug#42")
+        .atLeastOnce();
+    expect(commit.getId()).andReturn(commit).anyTimes();
+    expect(commit.getName()).andReturn("TestCommit").anyTimes();
+    expect(issueExtractor.getIssueIds("bug#4711, bug#42")).andReturn(
+        new String[] {"4711", "42"}).atLeastOnce();
+    expect(itsFacade.name()).andReturn("TestFacade").anyTimes();
+    expect(itsFacade.exists("4711")).andReturn(false).atLeastOnce();
+    expect(itsFacade.exists("42")).andReturn(false).atLeastOnce();
+
+    replayMocks();
+
+    try {
+      ivc.onCommitReceived(event);
+      fail("onCommitReceived did not throw any exception");
+    } catch (CommitValidationException e) {
+      assertTrue("Message of thrown CommitValidationException does not "
+          + "contain 'Non-existing'", e.getMessage().contains("Non-existing"));
+    }
+  }
+
+  public void testSuggestedMatchingMultipleIOExceptionIsNonExsting()
+      throws CommitValidationException, IOException {
+    List<CommitValidationMessage> ret;
+    ItsValidateComment ivc = injector.getInstance(ItsValidateComment.class);
+    ReceiveCommand command = createMock(ReceiveCommand.class);
+    RevCommit commit = createMock(RevCommit.class);
+    CommitReceivedEvent event = new CommitReceivedEvent(command, null, null,
+        commit, null);
+    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
+        ItsAssociationPolicy.OPTIONAL)).andReturn(
+            ItsAssociationPolicy.SUGGESTED).atLeastOnce();
+    expect(commit.getFullMessage()).andReturn("bug#4711, bug#42")
+        .atLeastOnce();
+    expect(commit.getId()).andReturn(commit).anyTimes();
+    expect(commit.getName()).andReturn("TestCommit").anyTimes();
+    expect(issueExtractor.getIssueIds("bug#4711, bug#42"))
+        .andReturn(new String[] {"4711", "42"}).atLeastOnce();
+    expect(itsFacade.name()).andReturn("TestFacade").anyTimes();
+    expect(itsFacade.exists("4711")).andThrow(new IOException("InjectedEx1"))
+        .atLeastOnce();
+    expect(itsFacade.exists("42")).andReturn(false).atLeastOnce();
+
+    replayMocks();
+
+    ret = ivc.onCommitReceived(event);
+
+    assertEquals("Size of returned CommitValidationMessages does not match",
+        2, ret.size());
+    assertTrue("First CommitValidationMessages does not contain " +
+        "'Failed'",ret.get(0).getMessage().contains("Failed"));
+    assertTrue("First CommitValidationMessages does not contain '4711'",
+        ret.get(0).getMessage().contains("4711"));
+    assertFalse("First CommitValidationMessages contains '42', although " +
+        "that bug exists", ret.get(0).getMessage().contains("42"));
+    assertTrue("Second CommitValidationMessages does not contain " +
+        "'Non-existing'",ret.get(1).getMessage().contains("Non-existing"));
+    assertTrue("Second CommitValidationMessages does not contain '4711'",
+        ret.get(1).getMessage().contains("4711"));
+    assertTrue("Second CommitValidationMessages does not contain '42'",
+        ret.get(1).getMessage().contains("42"));
+
+    assertLogMessageContains("4711");
+  }
+
+  public void assertEmptyList(List<CommitValidationMessage> list) {
+    if (!list.isEmpty()) {
+      StringBuffer sb = new StringBuffer();
+      sb.append("Commit Validation List is not emptyList is not empty, but contains:\n");
+      for (CommitValidationMessage msg : list) {
+        sb.append(msg.getMessage());
+        sb.append("\n");
+      }
+      fail(sb.toString());
+    }
+  }
+
+  private void setupCommonMocks() {
+    expect(issueExtractor.getPattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .anyTimes();
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+
+    injector = Guice.createInjector(new TestModule());
+
+    setupCommonMocks();
+  }
+
+  private class TestModule extends FactoryModule {
+    @Override
+    protected void configure() {
+      bind(String.class).annotatedWith(ItsName.class)
+          .toInstance("ItsTestName");
+
+      serverConfig = createMock(Config.class);
+      bind(Config.class).annotatedWith(GerritServerConfig.class)
+          .toInstance(serverConfig);
+
+      issueExtractor = createMock(IssueExtractor.class);
+      bind(IssueExtractor.class).toInstance(issueExtractor);
+
+      itsFacade = createMock(ItsFacade.class);
+      bind(ItsFacade.class).toInstance(itsFacade);
+    }
+  }
+}