Initial commit supporting Gerrit stable 2.9
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2f7896d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+target/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..080c4e8
--- /dev/null
+++ b/README.md
@@ -0,0 +1,105 @@
+Slack Integration Plugin
+========================
+
+A simple Gerrit plugin that allows the publishing of certain Gerrit events
+to a configured Slack Webhook URL. The plugin uses Gerrit's inherited project
+configuration support so common config options can be set at a higher level
+and shared by many projects along with project specific config options.
+
+
+Development
+-----------
+
+To build the plugin,
+[JDK 1.8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html),
+[Maven 3.0.x](http://maven.apache.org/download.cgi) and
+[Ant 1.9.x](https://ant.apache.org/bindownload.cgi) are required.
+Once installed use _mvn_ to build.
+
+    cd ./slack-integration
+    mvn install
+
+This command will compile/test and package the resulting artifact.
+
+    cd ./slack-integration
+    mvn package
+
+Once packaged, you can install the _./target/slack-integration.jar_ file into
+Gerrit.
+
+
+Installation
+------------
+
+Installing the Slack Integration Plugin is as simple as copying the resulting
+JAR file into the Gerrit plugins directory. Assuming you installation of Gerrit
+is located at _/usr/local/gerrit_ you simply execute the following.
+
+    cp ./slack-integration.jar /usr/local/gerrit/plugins
+
+Simple substitute the path to your Gerrit plugins directory as needed. Gerrit
+automatically loads new plugins and unloads old plugins, no restart is
+required.
+
+
+Configuration
+-------------
+
+The first thing you need to do is setup an incoming webhook integration in
+Slack. This is done via my.slack.com - Configure Integrations.
+
+Configuration of the Slack Integration Plugin is done in Gerrit via a project
+specific config file. This configuration is stored in the project’s
+_project.config_ file on the _refs/meta/config_ branch of the project.
+
+Common configuration options that can be shared between multiple projects
+can be placed in the _All-Projects_ config branch, or another project that
+serves as an inherited base. Config options can then be overridden in the
+actual project's config branch. For example, you may want to specify a default
+webhook URL, username and channel then override the channel to be specific
+to each project.
+
+Editing a project's config
+
+    mkdir <project>-config
+    cd <project>-config
+    git init
+    git remote add origin ssh://<admin-user>@<gerrit-host>:29418/<project>
+    git fetch origin refs/meta/config:refs/remotes/origin/meta/config
+    git checkout meta/config
+
+Create the following config block
+
+    vi project.config
+
+    [plugin "slack-integration"]
+        enabled = true
+        webhookurl = https://<web-hook-url>
+        channel = general
+        username = gerrit
+        ignore = "^WIP.*"
+
+Commit and push changes
+
+    git commit -a
+    git push origin meta/config:meta/config
+
+
+Configuration Options
+---------------------
+
+The following configuration options are available
+
+    enabled – boolean (true/false)
+        When true, enables Slack integration (defaults to false).
+    webhookurl - String
+        The Slack webhook URL to publish to (defaults to an
+        empty string).
+    channel - String
+        The Slack channel to publish to (defaults to "general").
+    username - String
+        The Slack username to publish as (defaults to "gerrit").
+    ignore - Pattern
+        A "dotall" enabled regular expression pattern that, when matches
+        against a commit message, will prevent the publishing of patchset
+        created event messages (defaults to an empty string).
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..1aac62d
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  ~ Copyright 2016 Cisco Systems, Inc.
+  ~
+  ~ 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.
+  ~
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>com.cisco.gerrit.plugins</groupId>
+    <artifactId>slack-integration</artifactId>
+    <packaging>jar</packaging>
+    <version>2.9</version>
+    <name>Slack Integration Plugin</name>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+
+        <gerrit-api-type>plugin</gerrit-api-type>
+        <gerrit-api-version>2.9</gerrit-api-version>
+    </properties>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>2.4</version>
+                <configuration>
+                    <archive>
+                        <manifestEntries>
+                            <Implementation-Vendor>
+                                Cisco Systems, Inc.
+                            </Implementation-Vendor>
+                            <Implementation-URL>
+                                https://gerrit-review.googlesource.com/#/admin/projects/plugins/slack-integration
+                            </Implementation-URL>
+                            <Implementation-Title>
+                                Slack Integration
+                            </Implementation-Title>
+                            <Implementation-Version>
+                                ${project.version}
+                            </Implementation-Version>
+                            <Gerrit-ApiType>
+                                ${gerrit-api-type}
+                            </Gerrit-ApiType>
+                            <Gerrit-ApiVersion>
+                                ${gerrit-api-version}
+                            </Gerrit-ApiVersion>
+                        </manifestEntries>
+                    </archive>
+                </configuration>
+            </plugin>
+
+            <!--<plugin>-->
+                <!--<groupId>org.apache.maven.plugins</groupId>-->
+                <!--<artifactId>maven-surefire-plugin</artifactId>-->
+                <!--<version>2.18.1</version>-->
+                <!--<configuration>-->
+                    <!--<excludes>-->
+                        <!--<excludes>**/*IntegrationTest.java</excludes>-->
+                    <!--</excludes>-->
+                <!--</configuration>-->
+            <!--</plugin>-->
+
+            <!--<plugin>-->
+                <!--<groupId>org.apache.maven.plugins</groupId>-->
+                <!--<artifactId>maven-failsafe-plugin</artifactId>-->
+                <!--<version>2.18.1</version>-->
+                <!--<configuration>-->
+                    <!--<includes>-->
+                        <!--<include>**/*IntegrationTest.java</include>-->
+                    <!--</includes>-->
+                <!--</configuration>-->
+            <!--</plugin>-->
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>2.3.2</version>
+                <configuration>
+                    <source>1.7</source>
+                    <target>1.7</target>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <repositories>
+        <repository>
+            <id>gerrit-api-repository</id>
+            <url>https://gerrit-api.commondatastorage.googleapis.com/release/
+            </url>
+        </repository>
+    </repositories>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.google.gerrit</groupId>
+            <artifactId>gerrit-${gerrit-api-type}-api</artifactId>
+            <version>${gerrit-api-version}</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <version>1.7.9</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.11</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <version>1.10.19</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.powermock</groupId>
+            <artifactId>powermock-module-junit4</artifactId>
+            <version>1.6.2</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.powermock</groupId>
+            <artifactId>powermock-api-mockito</artifactId>
+            <version>1.6.2</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/src/main/java/com/cisco/gerrit/plugins/slack/EventListener.java b/src/main/java/com/cisco/gerrit/plugins/slack/EventListener.java
new file mode 100644
index 0000000..b4c4ca5
--- /dev/null
+++ b/src/main/java/com/cisco/gerrit/plugins/slack/EventListener.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2016 Cisco Systems, Inc.
+ *
+ * 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.cisco.gerrit.plugins.slack;
+
+import com.cisco.gerrit.plugins.slack.client.WebhookClient;
+import com.cisco.gerrit.plugins.slack.config.ProjectConfig;
+import com.cisco.gerrit.plugins.slack.message.MessageGenerator;
+import com.cisco.gerrit.plugins.slack.message.MessageGeneratorFactory;
+import com.google.gerrit.common.ChangeListener;
+import com.google.gerrit.extensions.annotations.Listen;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.events.ChangeEvent;
+import com.google.gerrit.server.events.ChangeMergedEvent;
+import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Listens for Gerrit change events and publishes messages to Slack.
+ */
+@Listen
+@Singleton
+public class EventListener implements ChangeListener
+{
+    private static final Logger LOGGER =
+            LoggerFactory.getLogger(EventListener.class);
+
+    private static final String ALL_PROJECTS = "All-Projects";
+
+    @Inject
+    private PluginConfigFactory configFactory;
+
+    @Override
+    public void onChangeEvent(ChangeEvent event)
+    {
+        try
+        {
+            ProjectConfig config;
+            MessageGenerator messageGenerator;
+
+            if (event instanceof PatchSetCreatedEvent)
+            {
+                PatchSetCreatedEvent patchSetCreatedEvent;
+                patchSetCreatedEvent = (PatchSetCreatedEvent) event;
+
+                config = new ProjectConfig(configFactory,
+                        patchSetCreatedEvent.change.project);
+
+                messageGenerator = MessageGeneratorFactory.newInstance(
+                        patchSetCreatedEvent, config);
+            }
+            else if (event instanceof ChangeMergedEvent)
+            {
+                ChangeMergedEvent changeMergedEvent;
+                changeMergedEvent = (ChangeMergedEvent) event;
+
+                config = new ProjectConfig(configFactory,
+                        changeMergedEvent.change.project);
+
+                messageGenerator = MessageGeneratorFactory.newInstance(
+                        changeMergedEvent, config);
+            }
+            else
+            {
+                LOGGER.debug("Event " + event + " not currently supported");
+
+                config = new ProjectConfig(configFactory, ALL_PROJECTS);
+
+                messageGenerator = MessageGeneratorFactory.newInstance(
+                        event, config);
+            }
+
+            if (messageGenerator.shouldPublish())
+            {
+                WebhookClient client;
+                client = new WebhookClient();
+
+                client.publish(messageGenerator.generate(),
+                        config.getWebhookUrl());
+            }
+        }
+        catch (Throwable e)
+        {
+            LOGGER.error("Event " + event + " processing failed", e);
+        }
+    }
+}
diff --git a/src/main/java/com/cisco/gerrit/plugins/slack/client/WebhookClient.java b/src/main/java/com/cisco/gerrit/plugins/slack/client/WebhookClient.java
new file mode 100644
index 0000000..5ecb0fe
--- /dev/null
+++ b/src/main/java/com/cisco/gerrit/plugins/slack/client/WebhookClient.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2016 Cisco Systems, Inc.
+ *
+ * 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.cisco.gerrit.plugins.slack.client;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Scanner;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A minimal Slack client for publishing messages to a pre-configured incoming
+ * webhook (https://api.slack.com/incoming-webhooks).
+ *
+ * @author Matthew Montgomery
+ */
+public class WebhookClient
+{
+    /**
+     * The class logger instance.
+     **/
+    private static final Logger LOGGER =
+            LoggerFactory.getLogger(WebhookClient.class);
+
+    /**
+     * Publish a message to the provided Slack webhook URL.
+     *
+     * @param message    The message to publish.
+     * @param webhookUrl The web hook URL to publish to.
+     * @return true, if successful; otherwise false
+     */
+    public boolean publish(String message, String webhookUrl)
+    {
+        if (message == null || message.equals(""))
+        {
+            throw new IllegalArgumentException(
+                    "message cannot be null or empty");
+        }
+
+        if (webhookUrl == null || webhookUrl.equals(""))
+        {
+            throw new IllegalArgumentException(
+                    "webhookUrl cannot be null or empty");
+        }
+
+        boolean result;
+        result = false;
+
+        String response;
+        response = postRequest(message, webhookUrl);
+
+        if ("ok".equals(response))
+        {
+            result = true;
+        }
+        else
+        {
+            LOGGER.error("Unexpected response: [" + response + "].");
+        }
+
+        return result;
+    }
+
+    /**
+     * Initiates an HTTP POST to the provided Webhook URL.
+     *
+     * @param message    The message payload.
+     * @param webhookUrl The URL to post to.
+     * @return The response payload from Slack.
+     */
+    private String postRequest(String message, String webhookUrl)
+    {
+        String response;
+
+        HttpURLConnection connection;
+        connection = null;
+        try
+        {
+            connection = openConnection(webhookUrl);
+            try
+            {
+                connection.setRequestMethod("POST");
+                connection.setRequestProperty("Content-Type",
+                        "application/json");
+                connection.setRequestProperty("charset", "utf-8");
+
+                connection.setDoInput(true);
+                connection.setDoOutput(true);
+
+                DataOutputStream request;
+                request = new DataOutputStream(connection.getOutputStream());
+
+                request.writeBytes(message);
+                request.flush();
+                request.close();
+            }
+            catch (IOException e)
+            {
+                throw new RuntimeException(
+                        "Error posting message to Slack: [" + e.getMessage() +
+                                "].", e);
+            }
+
+            response = getResponse(connection);
+        }
+        finally
+        {
+            if (connection != null)
+            {
+                connection.disconnect();
+            }
+        }
+
+        return response;
+    }
+
+    /**
+     * Opens a connection to the provided Webhook URL.
+     *
+     * @param webhookUrl The Webhook URL to open a connection to.
+     * @return The open connection to the provided Webhook URL.
+     */
+    private HttpURLConnection openConnection(String webhookUrl)
+    {
+        try
+        {
+            return (HttpURLConnection) new URL(webhookUrl).openConnection();
+        }
+        catch (MalformedURLException e)
+        {
+            throw new RuntimeException("Unable to create webhook URL: " +
+                    webhookUrl, e);
+        }
+        catch (IOException e)
+        {
+            throw new RuntimeException(
+                    "Error opening connection to Slack URL: [" +
+                            e.getMessage() + "].", e);
+        }
+    }
+
+    /**
+     * Gets the response payload.
+     *
+     * @param connection The connection.
+     * @return The string representation of the response.
+     */
+    private String getResponse(HttpURLConnection connection)
+    {
+        String response;
+
+        InputStream responseStream;
+        responseStream = null;
+        try
+        {
+            responseStream = connection.getInputStream();
+            response = readResponse(responseStream);
+        }
+        catch (IOException e)
+        {
+            responseStream = connection.getErrorStream();
+            response = readResponse(responseStream);
+        }
+        finally
+        {
+            if (responseStream != null)
+            {
+                try
+                {
+                    responseStream.close();
+                }
+                catch (IOException e)
+                {
+                    LOGGER.debug("Error closing response stream: " +
+                            e.getMessage());
+                }
+            }
+        }
+
+        return response;
+    }
+
+    /**
+     * Reads the response from the response InputStream.
+     *
+     * @param responseStream The response stream from the connection.
+     * @return The string representation of the response.
+     */
+    private String readResponse(InputStream responseStream)
+    {
+        try
+        {
+            Scanner scanner;
+            scanner = new Scanner(responseStream, "UTF-8");
+            scanner.useDelimiter("\\A");
+
+            return scanner.next();
+        }
+        catch (Exception e)
+        {
+            throw new RuntimeException(
+                    "Error reading response: [" + e.getMessage() + "].", e);
+        }
+    }
+}
diff --git a/src/main/java/com/cisco/gerrit/plugins/slack/config/ProjectConfig.java b/src/main/java/com/cisco/gerrit/plugins/slack/config/ProjectConfig.java
new file mode 100644
index 0000000..6f79f5b
--- /dev/null
+++ b/src/main/java/com/cisco/gerrit/plugins/slack/config/ProjectConfig.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2016 Cisco Systems, Inc.
+ *
+ * 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.cisco.gerrit.plugins.slack.config;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A simple configuration class to access plugin config values.
+ *
+ * @author Matthew Montgomery
+ */
+public class ProjectConfig
+{
+    /**
+     * The class logger instance.
+     */
+    private static final Logger LOGGER = LoggerFactory.getLogger(
+            ProjectConfig.class);
+
+    /**
+     * The name of the plugin config section to lookup within the gerrit.config
+     * file.
+     */
+    public static final String CONFIG_NAME = "slack-integration";
+
+    private boolean enabled;
+    private String webhookUrl;
+    private String channel;
+    private String username;
+    private String ignore;
+
+    /**
+     * Creates a new instance of the ProjectConfig class for the given project.
+     *
+     * @param configFactory The Gerrit PluginConfigFactory instance to use.
+     * @param project The project to use when looking up a configuration.
+     */
+    public ProjectConfig(PluginConfigFactory configFactory, String project)
+    {
+        enabled = false;
+
+        Project.NameKey projectNameKey;
+        projectNameKey = Project.NameKey.parse(project);
+
+        try
+        {
+            enabled = configFactory.getFromProjectConfigWithInheritance(
+                    projectNameKey, CONFIG_NAME).getBoolean(
+                    "enabled", false);
+
+            webhookUrl = configFactory.getFromProjectConfigWithInheritance(
+                    projectNameKey, CONFIG_NAME).getString(
+                    "webhookurl", "");
+
+            channel = configFactory.getFromProjectConfigWithInheritance(
+                    projectNameKey, CONFIG_NAME).getString(
+                    "channel", "general");
+
+            username = configFactory.getFromProjectConfigWithInheritance(
+                    projectNameKey, CONFIG_NAME).getString(
+                    "username", "gerrit");
+
+            ignore = configFactory.getFromProjectConfigWithInheritance(
+                    projectNameKey, CONFIG_NAME).getString(
+                    "ignore", "");
+        }
+        catch (NoSuchProjectException e)
+        {
+            LOGGER.warn("The specified project could not be found: " +
+                    project);
+        }
+    }
+
+    public boolean isEnabled()
+    {
+        return enabled;
+    }
+
+    public String getWebhookUrl()
+    {
+        return webhookUrl;
+    }
+
+    public String getChannel()
+    {
+        return channel;
+    }
+
+    public String getUsername()
+    {
+        return username;
+    }
+
+    public String getIgnore()
+    {
+        return ignore;
+    }
+}
diff --git a/src/main/java/com/cisco/gerrit/plugins/slack/message/ChangeMergedMessageGenerator.java b/src/main/java/com/cisco/gerrit/plugins/slack/message/ChangeMergedMessageGenerator.java
new file mode 100644
index 0000000..4e0882f
--- /dev/null
+++ b/src/main/java/com/cisco/gerrit/plugins/slack/message/ChangeMergedMessageGenerator.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2016 Cisco Systems, Inc.
+ *
+ * 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.cisco.gerrit.plugins.slack.message;
+
+import com.cisco.gerrit.plugins.slack.config.ProjectConfig;
+import com.cisco.gerrit.plugins.slack.util.ResourceHelper;
+import com.google.gerrit.server.events.ChangeMergedEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A specific MessageGenerator implementation that can generate a message for
+ * a change merged event.
+ *
+ * @author Matthew Montgomery
+ */
+public class ChangeMergedMessageGenerator extends MessageGenerator
+{
+    /**
+     * The class logger instance.
+     */
+    private static final Logger LOGGER =
+            LoggerFactory.getLogger(ChangeMergedMessageGenerator.class);
+
+    private ProjectConfig config;
+    private ChangeMergedEvent event;
+
+    /**
+     * Creates a new ChangeMergedMessageGenerator instance using the provided
+     * ChangeMergedEvent instance.
+     *
+     * @param event The ChangeMergedEvent instance to generate a message for.
+     */
+    protected ChangeMergedMessageGenerator(ChangeMergedEvent event,
+            ProjectConfig config)
+    {
+        if (event == null)
+        {
+            throw new NullPointerException("event cannot be null");
+        }
+
+        this.event = event;
+        this.config = config;
+    }
+
+    @Override
+    public boolean shouldPublish()
+    {
+        return true;
+    }
+
+    @Override
+    public String generate()
+    {
+        String message;
+        message = "";
+
+        try
+        {
+            String template;
+            template = ResourceHelper.loadNamedResourceAsString(
+                    "basic-message-template.json");
+
+            StringBuilder text;
+            text = new StringBuilder();
+
+            text.append(escape(event.submitter.name));
+            text.append(" merged\\n>>>");
+            text.append(escape(event.change.project));
+            text.append(" (");
+            text.append(escape(event.change.branch));
+            text.append("): ");
+            text.append(escape(event.change.commitMessage.split("\n")[0]));
+            text.append(" (");
+            text.append(escape(event.change.url));
+            text.append(")");
+
+            message = String.format(template, text, config.getChannel(),
+                    config.getUsername());
+        }
+        catch (Exception e)
+        {
+            LOGGER.error("Error generating message: " + e.getMessage());
+        }
+
+        return message;
+    }
+}
diff --git a/src/main/java/com/cisco/gerrit/plugins/slack/message/MessageGenerator.java b/src/main/java/com/cisco/gerrit/plugins/slack/message/MessageGenerator.java
new file mode 100644
index 0000000..16b5b4f
--- /dev/null
+++ b/src/main/java/com/cisco/gerrit/plugins/slack/message/MessageGenerator.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2016 Cisco Systems, Inc.
+ *
+ * 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.cisco.gerrit.plugins.slack.message;
+
+/**
+ * Defines a simple base class for a message generators.
+ *
+ * @author Matthew Montgomery
+ */
+public abstract class MessageGenerator
+{
+    /**
+     * Whether or not the generated message should be published.
+     *
+     * @return True if the message should be published, otherwise false
+     */
+    public abstract boolean shouldPublish();
+
+    /**
+     * Generates an event specific message suitable for publishing.
+     *
+     * @return The generated message.
+     */
+    public abstract String generate();
+
+    /**
+     * Escapes the double quote character.
+     *
+     * @param message The message in which to search escape double quote
+     *                characters
+     *
+     * @return The message with all occurrences of the double quote character
+     * escaped.
+     */
+    protected String escape(String message)
+    {
+        if (message != null)
+        {
+            message = message.replace("\"", "\\\"");
+        }
+
+        return message;
+    }
+}
diff --git a/src/main/java/com/cisco/gerrit/plugins/slack/message/MessageGeneratorFactory.java b/src/main/java/com/cisco/gerrit/plugins/slack/message/MessageGeneratorFactory.java
new file mode 100644
index 0000000..5e026db
--- /dev/null
+++ b/src/main/java/com/cisco/gerrit/plugins/slack/message/MessageGeneratorFactory.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2016 Cisco Systems, Inc.
+ *
+ * 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.cisco.gerrit.plugins.slack.message;
+
+import com.cisco.gerrit.plugins.slack.config.ProjectConfig;
+import com.google.gerrit.server.events.ChangeEvent;
+import com.google.gerrit.server.events.ChangeMergedEvent;
+import com.google.gerrit.server.events.PatchSetCreatedEvent;
+
+/**
+ * Factory used to create event specific MessageGenerator instances.
+ *
+ * @author Matthew Montgomery
+ */
+public class MessageGeneratorFactory
+{
+    // Made private to prevent instantiation
+    private MessageGeneratorFactory() {}
+
+    /**
+     * Creates a new MessageGenerator for patchset created events.
+     *
+     * @param event A PatchSetCreatedEvent instance
+     * @param config A ProjectConfig instance for the given event
+     *
+     * @return A MessageGenerator instance capable of generating a message for
+     * a PatchSetCreatedEvent.
+     */
+    public static MessageGenerator newInstance(PatchSetCreatedEvent event,
+            ProjectConfig config)
+    {
+        PatchSetCreatedMessageGenerator messageGenerator;
+        messageGenerator = new PatchSetCreatedMessageGenerator(event, config);
+
+        return messageGenerator;
+    }
+
+    /**
+     * Creates a new MessageGenerator for change merged events.
+     *
+     * @param event A ChangeMergedEvent instance
+     * @param config A ProjectConfig instance for the given event
+     *
+     * @return A MessageGenerator instance capable of generating a message for
+     * a ChangeMergedEvent.
+     */
+    public static MessageGenerator newInstance(ChangeMergedEvent event,
+            ProjectConfig config)
+    {
+        ChangeMergedMessageGenerator messageGenerator;
+        messageGenerator = new ChangeMergedMessageGenerator(event, config);
+
+        return messageGenerator;
+    }
+
+    /**
+     * Creates a new MessageGenerator for unsupported events.
+     *
+     * @param event A ChangeEvent instance
+     * @param config A ProjectConfig instance for the given event
+     *
+     * @return A MessageGenerator instance capable of generating a message for
+     * an unsupported ChangeEvent.
+     */
+    public static MessageGenerator newInstance(ChangeEvent event,
+            ProjectConfig config)
+    {
+        UnsupportedMessageGenerator messageGenerator;
+        messageGenerator = new UnsupportedMessageGenerator(event, config);
+
+        return messageGenerator;
+    }
+}
diff --git a/src/main/java/com/cisco/gerrit/plugins/slack/message/PatchSetCreatedMessageGenerator.java b/src/main/java/com/cisco/gerrit/plugins/slack/message/PatchSetCreatedMessageGenerator.java
new file mode 100644
index 0000000..f10b7f9
--- /dev/null
+++ b/src/main/java/com/cisco/gerrit/plugins/slack/message/PatchSetCreatedMessageGenerator.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2016 Cisco Systems, Inc.
+ *
+ * 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.cisco.gerrit.plugins.slack.message;
+
+import com.cisco.gerrit.plugins.slack.config.ProjectConfig;
+import com.cisco.gerrit.plugins.slack.util.ResourceHelper;
+import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A specific MessageGenerator implementation that can generate a message for a
+ * patchset created event.
+ *
+ * @author Matthew Montgomery
+ */
+public class PatchSetCreatedMessageGenerator extends MessageGenerator
+{
+    private static final Logger LOGGER =
+            LoggerFactory.getLogger(PatchSetCreatedMessageGenerator.class);
+
+    private PatchSetCreatedEvent event;
+    private ProjectConfig config;
+
+    /**
+     * Creates a new PatchSetCreatedMessageGenerator instance using the
+     * provided PatchSetCreatedEvent instance.
+     *
+     * @param event The PatchSetCreatedEvent instance to generate a
+     *              message for.
+     */
+    protected PatchSetCreatedMessageGenerator(PatchSetCreatedEvent event,
+            ProjectConfig config)
+    {
+        if (event == null)
+        {
+            throw new NullPointerException("event cannot be null");
+        }
+
+        this.event = event;
+        this.config = config;
+    }
+
+    @Override
+    public boolean shouldPublish()
+    {
+        boolean result;
+        result = true;
+
+        try
+        {
+            Pattern pattern;
+            pattern = Pattern.compile(config.getIgnore(), Pattern.DOTALL);
+
+            Matcher matcher;
+            matcher = pattern.matcher(event.change.commitMessage);
+
+            // If the ignore pattern matches, publishing should not happen
+            result = !matcher.matches();
+        }
+        catch (Exception e)
+        {
+            LOGGER.warn("The specified ignore pattern was invalid", e);
+        }
+
+        return result;
+    }
+
+    @Override
+    public String generate()
+    {
+        String message;
+        message = "";
+
+        try
+        {
+            String template;
+            template = ResourceHelper.loadNamedResourceAsString(
+                    "basic-message-template.json");
+
+            StringBuilder text;
+            text = new StringBuilder();
+
+            text.append(escape(event.uploader.name));
+            text.append(" proposed\\n>>>");
+            text.append(escape(event.change.project));
+            text.append(" (");
+            text.append(escape(event.change.branch));
+            text.append("): ");
+            text.append(escape(event.change.commitMessage.split("\n")[0]));
+            text.append(" (");
+            text.append(escape(event.change.url));
+            text.append(")");
+
+            message = String.format(template, text, config.getChannel(),
+                    config.getUsername());
+        }
+        catch (Exception e)
+        {
+            LOGGER.error("Error generating message: " + e.getMessage());
+        }
+
+        return message;
+    }
+}
diff --git a/src/main/java/com/cisco/gerrit/plugins/slack/message/UnsupportedMessageGenerator.java b/src/main/java/com/cisco/gerrit/plugins/slack/message/UnsupportedMessageGenerator.java
new file mode 100644
index 0000000..6dcd767
--- /dev/null
+++ b/src/main/java/com/cisco/gerrit/plugins/slack/message/UnsupportedMessageGenerator.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2016 Cisco Systems, Inc.
+ *
+ * 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.cisco.gerrit.plugins.slack.message;
+
+import com.cisco.gerrit.plugins.slack.config.ProjectConfig;
+import com.google.gerrit.server.events.ChangeEvent;
+
+/**
+ * A specific MessageGenerator implementation that can generate a message for
+ * an unsupported ChangeEvent. The default behavior for this MessageGenerator
+ * is to flag that it should not be published.
+ *
+ * @author Matthew Montgomery
+ */
+public class UnsupportedMessageGenerator extends MessageGenerator
+{
+    private ProjectConfig config;
+    private ChangeEvent event;
+
+    protected UnsupportedMessageGenerator(ChangeEvent event,
+            ProjectConfig config)
+    {
+        if (event == null)
+        {
+            throw new NullPointerException("event cannot be null");
+        }
+
+        this.event = event;
+        this.config = config;
+    }
+
+    @Override
+    public boolean shouldPublish()
+    {
+        return false;
+    }
+
+    @Override
+    public String generate()
+    {
+        StringBuilder message;
+        message = new StringBuilder();
+
+        message.append("Unsupported change event: ");
+        message.append(this.event.toString());
+
+        return message.toString();
+    }
+}
diff --git a/src/main/java/com/cisco/gerrit/plugins/slack/util/ResourceHelper.java b/src/main/java/com/cisco/gerrit/plugins/slack/util/ResourceHelper.java
new file mode 100644
index 0000000..b7482dc
--- /dev/null
+++ b/src/main/java/com/cisco/gerrit/plugins/slack/util/ResourceHelper.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2016 Cisco Systems, Inc.
+ *
+ * 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.cisco.gerrit.plugins.slack.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Simple helper class to load resources via the current classloader.
+ */
+public final class ResourceHelper
+{
+    // Made private to prevent instantiation.
+    private ResourceHelper() { }
+
+    /**
+     * Loads the named resource as an InputStream from the current
+     * classloader.
+     *
+     * @param name The named resource.
+     *
+     * @return The named resource as an InputStream, null if not found.
+     *
+     * @throws IOException In the event of an IO error
+     */
+    public static InputStream loadNamedResourceAsStream(String name)
+            throws IOException
+    {
+        InputStream result;
+        result = null;
+
+        if (name != null)
+        {
+            ClassLoader classLoader;
+            classLoader = ResourceHelper.class.getClassLoader();
+
+            result = classLoader.getResourceAsStream(name);
+        }
+
+        return result;
+    }
+
+    /**
+     * Loads the named resource as a String from the current classloader.
+     *
+     * @param name The named resource.
+     *
+     * @return The named resource as a String, null if not found.
+     *
+     * @throws IOException In the event of an IO error
+     */
+    public static String loadNamedResourceAsString(String name)
+            throws IOException
+    {
+        String result;
+        result = null;
+
+        InputStream inputStream;
+        inputStream = ResourceHelper.loadNamedResourceAsStream(name);
+
+        if (inputStream != null)
+        {
+            StringBuffer buffer;
+            buffer = new StringBuffer();
+
+            byte[] b;
+            b = new byte[4096];
+
+            for (int n; (n = inputStream.read(b)) != -1; )
+            {
+                buffer.append(new String(b, 0, n));
+            }
+
+            result = buffer.toString();
+        }
+
+        return result;
+    }
+}
diff --git a/src/main/resources/basic-message-template.json b/src/main/resources/basic-message-template.json
new file mode 100644
index 0000000..f61b2bf
--- /dev/null
+++ b/src/main/resources/basic-message-template.json
@@ -0,0 +1 @@
+{"text": "%s","channel": "#%s","username": "%s"}
diff --git a/src/test/java/com/cisco/gerrit/plugins/slack/EventListenerTest.java b/src/test/java/com/cisco/gerrit/plugins/slack/EventListenerTest.java
new file mode 100644
index 0000000..bed0523
--- /dev/null
+++ b/src/test/java/com/cisco/gerrit/plugins/slack/EventListenerTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2016 Cisco Systems, Inc.
+ *
+ * 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.cisco.gerrit.plugins.slack;
+
+import com.google.gerrit.server.events.ChangeMergedEvent;
+import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+public class EventListenerTest
+{
+    private PatchSetCreatedEvent mockPatchSetCreatedEvent =
+            mock(PatchSetCreatedEvent.class);
+    private ChangeMergedEvent mocChangeMergedEvent =
+            mock(ChangeMergedEvent.class);
+
+    private EventListener eventListener;
+
+    @Before
+    public void setup() throws Exception
+    {
+        eventListener = new EventListener();
+    }
+
+    @Test
+    public void handlesPatchSetCreatedEvents() throws Exception
+    {
+        // Wat? Placeholder, as I need to think about out how to test...
+        assertTrue(true);
+    }
+}
diff --git a/src/test/java/com/cisco/gerrit/plugins/slack/client/WebhookClientIntegrationTest.java b/src/test/java/com/cisco/gerrit/plugins/slack/client/WebhookClientIntegrationTest.java
new file mode 100644
index 0000000..c52651c
--- /dev/null
+++ b/src/test/java/com/cisco/gerrit/plugins/slack/client/WebhookClientIntegrationTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2016 Cisco Systems, Inc.
+ *
+ * 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.cisco.gerrit.plugins.slack.client;
+
+import com.cisco.gerrit.plugins.slack.util.ResourceHelper;
+import org.junit.Test;
+
+import java.io.InputStream;
+import java.util.Properties;
+
+import static org.junit.Assert.assertTrue;
+
+public class WebhookClientIntegrationTest
+{
+    @Test
+    public void canPublishMessage() throws Exception
+    {
+        WebhookClient client;
+        client = new WebhookClient();
+
+        InputStream testProperties;
+        testProperties = ResourceHelper.loadNamedResourceAsStream(
+                "test.properties");
+
+        Properties properties;
+        properties = new Properties();
+        properties.load(testProperties);
+
+        testProperties.close();
+
+        String message;
+        message = "{\"text\": \"Integration Test Message\"}";
+
+        String webhookUrl;
+        webhookUrl = properties.getProperty("webhook-url");
+
+        assertTrue(client.publish(message, webhookUrl));
+    }
+}
diff --git a/src/test/java/com/cisco/gerrit/plugins/slack/config/ProjectConfigTest.java b/src/test/java/com/cisco/gerrit/plugins/slack/config/ProjectConfigTest.java
new file mode 100644
index 0000000..3a89b3b
--- /dev/null
+++ b/src/test/java/com/cisco/gerrit/plugins/slack/config/ProjectConfigTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2016 Cisco Systems, Inc.
+ *
+ * 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.cisco.gerrit.plugins.slack.config;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for the PluginConfig class.
+ */
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({Project.NameKey.class})
+public class ProjectConfigTest
+{
+    private static final String PROJECT_NAME = "test-project";
+
+    private Project.NameKey mockNameKey =
+            mock(Project.NameKey.class);
+
+    private PluginConfigFactory mockConfigFactory =
+            mock(PluginConfigFactory.class);
+
+    private PluginConfig mockPluginConfig =
+            mock(PluginConfig.class);
+
+    private ProjectConfig config;
+
+    @Before
+    public void setup() throws Exception
+    {
+        PowerMockito.mockStatic(Project.NameKey.class);
+        when(Project.NameKey.parse(PROJECT_NAME)).thenReturn(mockNameKey);
+
+        Project.NameKey projectNameKey;
+        projectNameKey = Project.NameKey.parse(PROJECT_NAME);
+
+        // Setup mocks
+        when(mockConfigFactory.getFromProjectConfigWithInheritance(
+                projectNameKey, ProjectConfig.CONFIG_NAME))
+                .thenReturn(mockPluginConfig);
+
+        when(mockPluginConfig.getBoolean("enabled", false))
+                .thenReturn(true);
+        when(mockPluginConfig.getString("webhookurl", ""))
+                .thenReturn("https://webook/");
+        when(mockPluginConfig.getString("channel", "general"))
+                .thenReturn("test-channel");
+        when(mockPluginConfig.getString("username", "gerrit"))
+                .thenReturn("test-user");
+        when(mockPluginConfig.getString("ignore", ""))
+                .thenReturn("^WIP.*");
+
+        config = new ProjectConfig(mockConfigFactory, PROJECT_NAME);
+    }
+
+    @Test
+    public void testIsEnabled() throws Exception
+    {
+        assertTrue(config.isEnabled());
+    }
+
+    @Test
+    public void testGetWebhookUrl() throws Exception
+    {
+        assertThat(config.getWebhookUrl(), is(equalTo("https://webook/")));
+    }
+
+    @Test
+    public void testGetChannel() throws Exception
+    {
+        assertThat(config.getChannel(), is(equalTo("test-channel")));
+    }
+
+    @Test
+    public void testGetUsername() throws Exception
+    {
+        assertThat(config.getUsername(), is(equalTo("test-user")));
+    }
+}
diff --git a/src/test/java/com/cisco/gerrit/plugins/slack/message/ChangeMergedMessageGeneratorTest.java b/src/test/java/com/cisco/gerrit/plugins/slack/message/ChangeMergedMessageGeneratorTest.java
new file mode 100644
index 0000000..49ee3db
--- /dev/null
+++ b/src/test/java/com/cisco/gerrit/plugins/slack/message/ChangeMergedMessageGeneratorTest.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2016 Cisco Systems, Inc.
+ *
+ * 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.cisco.gerrit.plugins.slack.message;
+
+import com.cisco.gerrit.plugins.slack.config.ProjectConfig;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.events.ChangeMergedEvent;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for the ChangeMergedMessageGeneratorTest class. The expected behavior
+ * is that the ChangeMergedMessageGenerator should publish regardless of a
+ * configured ignore pattern.
+ */
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({Project.NameKey.class})
+public class ChangeMergedMessageGeneratorTest
+{
+    private static final String PROJECT_NAME = "test-project";
+
+    private Project.NameKey mockNameKey =
+            mock(Project.NameKey.class);
+
+    private PluginConfigFactory mockConfigFactory =
+            mock(PluginConfigFactory.class);
+
+    private PluginConfig mockPluginConfig =
+            mock(PluginConfig.class);
+
+    private ChangeMergedEvent mockEvent = mock(ChangeMergedEvent.class);
+    private AccountAttribute mockAccount = mock(AccountAttribute.class);
+    private ChangeAttribute mockChange = mock(ChangeAttribute.class);
+
+    private ProjectConfig config;
+
+    @Before
+    public void setup() throws Exception
+    {
+        PowerMockito.mockStatic(Project.NameKey.class);
+        when(Project.NameKey.parse(PROJECT_NAME)).thenReturn(mockNameKey);
+
+        Project.NameKey projectNameKey;
+        projectNameKey = Project.NameKey.parse(PROJECT_NAME);
+
+        // Setup mocks
+        when(mockConfigFactory.getFromProjectConfigWithInheritance(
+                projectNameKey, ProjectConfig.CONFIG_NAME))
+                .thenReturn(mockPluginConfig);
+
+        when(mockPluginConfig.getBoolean("enabled", false))
+                .thenReturn(true);
+        when(mockPluginConfig.getString("webhookurl", ""))
+                .thenReturn("https://webook/");
+        when(mockPluginConfig.getString("channel", "general"))
+                .thenReturn("testchannel");
+        when(mockPluginConfig.getString("username", "gerrit"))
+                .thenReturn("testuser");
+        when(mockPluginConfig.getString("ignore", ""))
+                .thenReturn("^WIP.*");
+
+        config = new ProjectConfig(mockConfigFactory, PROJECT_NAME);
+    }
+
+    @Test
+    public void factoryCreatesExpectedType() throws Exception
+    {
+        MessageGenerator messageGenerator;
+        messageGenerator = MessageGeneratorFactory.newInstance(
+                mockEvent, config);
+
+        assertThat(messageGenerator instanceof ChangeMergedMessageGenerator,
+                is(true));
+    }
+
+    @Test
+    public void publishesWhenExpected() throws Exception
+    {
+        // Setup mocks
+        mockEvent.change = mockChange;
+        mockChange.commitMessage = "This is a title\nAnd a the body.";
+
+        // Test
+        MessageGenerator messageGenerator;
+        messageGenerator = MessageGeneratorFactory.newInstance(
+                mockEvent, config);
+
+        assertThat(messageGenerator.shouldPublish(), is(true));
+    }
+
+    @Test
+    public void doesNotPublishWhenExpected() throws Exception
+    {
+        // Setup mocks
+        mockEvent.change = mockChange;
+        mockChange.commitMessage = "WIP:This is a title\nAnd a the body.";
+
+        // Test
+        MessageGenerator messageGenerator;
+        messageGenerator = MessageGeneratorFactory.newInstance(
+                mockEvent, config);
+
+        assertThat(messageGenerator.shouldPublish(), is(true));
+    }
+
+    @Test
+    public void handlesInvalidIgnorePatterns() throws Exception
+    {
+        when(mockPluginConfig.getString("ignore", ""))
+                .thenReturn(null);
+
+        // Test
+        MessageGenerator messageGenerator;
+        messageGenerator = MessageGeneratorFactory.newInstance(
+                mockEvent, config);
+
+        assertThat(messageGenerator.shouldPublish(), is(true));
+    }
+
+    @Test
+    public void generatesExpectedMessage() throws Exception
+    {
+        // Setup mocks
+        mockEvent.change = mockChange;
+        mockEvent.submitter = mockAccount;
+
+        mockChange.project = "testproject";
+        mockChange.branch = "master";
+        mockChange.url = "https://change/";
+        mockChange.commitMessage = "This is a title\nAnd a the body.";
+
+        mockAccount.name = "Unit Tester";
+
+        // Test
+        MessageGenerator messageGenerator;
+        messageGenerator = MessageGeneratorFactory.newInstance(
+                mockEvent, config);
+
+        String expectedResult;
+        expectedResult = "{\"text\": \"Unit Tester merged\\n>>>" +
+                "testproject (master): This is a title (https://change/)\"," +
+                "\"channel\": \"#testchannel\",\"username\": \"testuser\"}\n";
+
+        String actualResult;
+        actualResult = messageGenerator.generate();
+
+        assertThat(actualResult, is(equalTo(expectedResult)));
+    }
+}
diff --git a/src/test/java/com/cisco/gerrit/plugins/slack/message/MessageGeneratorTest.java b/src/test/java/com/cisco/gerrit/plugins/slack/message/MessageGeneratorTest.java
new file mode 100644
index 0000000..63be613
--- /dev/null
+++ b/src/test/java/com/cisco/gerrit/plugins/slack/message/MessageGeneratorTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016 Cisco Systems, Inc.
+ *
+ * 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.cisco.gerrit.plugins.slack.message;
+
+import org.junit.Test;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.junit.Assert.assertThat;
+
+public class MessageGeneratorTest
+{
+
+    @Test
+    public void testEscape() throws Exception
+    {
+        MessageGenerator messageGenerator;
+        messageGenerator = new MessageGenerator()
+        {
+            @Override
+            public boolean shouldPublish()
+            {
+                return false;
+            }
+
+            @Override
+            public String generate()
+            {
+                return null;
+            }
+        };
+
+        assertThat(messageGenerator.escape("\""), is(equalTo("\\\"")));
+    }
+}
diff --git a/src/test/java/com/cisco/gerrit/plugins/slack/message/PatchSetCreatedMessageGeneratorTest.java b/src/test/java/com/cisco/gerrit/plugins/slack/message/PatchSetCreatedMessageGeneratorTest.java
new file mode 100644
index 0000000..e534600
--- /dev/null
+++ b/src/test/java/com/cisco/gerrit/plugins/slack/message/PatchSetCreatedMessageGeneratorTest.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2016 Cisco Systems, Inc.
+ *
+ * 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.cisco.gerrit.plugins.slack.message;
+
+import com.cisco.gerrit.plugins.slack.config.ProjectConfig;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for the PatchSetCreatedMessageGeneratorTest class.
+ */
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({Project.NameKey.class})
+public class PatchSetCreatedMessageGeneratorTest
+{
+    private static final String PROJECT_NAME = "test-project";
+
+    private Project.NameKey mockNameKey =
+            mock(Project.NameKey.class);
+
+    private PluginConfigFactory mockConfigFactory =
+            mock(PluginConfigFactory.class);
+
+    private PluginConfig mockPluginConfig =
+            mock(PluginConfig.class);
+
+    private PatchSetCreatedEvent mockEvent = mock(PatchSetCreatedEvent.class);
+    private AccountAttribute mockAccount = mock(AccountAttribute.class);
+    private ChangeAttribute mockChange = mock(ChangeAttribute.class);
+
+    private ProjectConfig config;
+
+    @Before
+    public void setup() throws Exception
+    {
+        PowerMockito.mockStatic(Project.NameKey.class);
+        when(Project.NameKey.parse(PROJECT_NAME)).thenReturn(mockNameKey);
+
+        Project.NameKey projectNameKey;
+        projectNameKey = Project.NameKey.parse(PROJECT_NAME);
+
+        // Setup mocks
+        when(mockConfigFactory.getFromProjectConfigWithInheritance(
+                projectNameKey, ProjectConfig.CONFIG_NAME))
+                .thenReturn(mockPluginConfig);
+
+        when(mockPluginConfig.getBoolean("enabled", false))
+                .thenReturn(true);
+        when(mockPluginConfig.getString("webhookurl", ""))
+                .thenReturn("https://webook/");
+        when(mockPluginConfig.getString("channel", "general"))
+                .thenReturn("testchannel");
+        when(mockPluginConfig.getString("username", "gerrit"))
+                .thenReturn("testuser");
+        when(mockPluginConfig.getString("ignore", ""))
+                .thenReturn("^WIP.*");
+
+        config = new ProjectConfig(mockConfigFactory, PROJECT_NAME);
+    }
+
+    @Test
+    public void factoryCreatesExpectedType() throws Exception
+    {
+        MessageGenerator messageGenerator;
+        messageGenerator = MessageGeneratorFactory.newInstance(
+                mockEvent, config);
+
+        assertThat(messageGenerator instanceof PatchSetCreatedMessageGenerator,
+                is(true));
+    }
+
+    @Test
+    public void publishesWhenExpected() throws Exception
+    {
+        // Setup mocks
+        mockEvent.change = mockChange;
+        mockChange.commitMessage = "This is a title\nAnd a the body.";
+
+        // Test
+        MessageGenerator messageGenerator;
+        messageGenerator = MessageGeneratorFactory.newInstance(
+                mockEvent, config);
+
+        assertThat(messageGenerator.shouldPublish(), is(true));
+    }
+
+    @Test
+    public void doesNotPublishWhenExpected() throws Exception
+    {
+        // Setup mocks
+        mockEvent.change = mockChange;
+        mockChange.commitMessage = "WIP-This is a title\nAnd a the body.";
+
+        // Test
+        MessageGenerator messageGenerator;
+        messageGenerator = MessageGeneratorFactory.newInstance(
+                mockEvent, config);
+
+        assertThat(messageGenerator.shouldPublish(), is(false));
+    }
+
+    @Test
+    public void handlesInvalidIgnorePatterns() throws Exception
+    {
+        when(mockPluginConfig.getString("ignore", ""))
+                .thenReturn(null);
+
+        // Test
+        MessageGenerator messageGenerator;
+        messageGenerator = MessageGeneratorFactory.newInstance(
+                mockEvent, config);
+
+        assertThat(messageGenerator.shouldPublish(), is(true));
+    }
+
+    @Test
+    public void generatesExpectedMessage() throws Exception
+    {
+        // Setup mocks
+        mockEvent.change = mockChange;
+        mockEvent.uploader = mockAccount;
+
+        mockChange.project = "testproject";
+        mockChange.branch = "master";
+        mockChange.url = "https://change/";
+        mockChange.commitMessage = "This is a title\nAnd a the body.";
+
+        mockAccount.name = "Unit Tester";
+
+        // Test
+        MessageGenerator messageGenerator;
+        messageGenerator = MessageGeneratorFactory.newInstance(
+                mockEvent, config);
+
+        String expectedResult;
+        expectedResult = "{\"text\": \"Unit Tester proposed\\n>>>" +
+                "testproject (master): This is a title (https://change/)\"," +
+                "\"channel\": \"#testchannel\",\"username\": \"testuser\"}\n";
+
+        String actualResult;
+        actualResult = messageGenerator.generate();
+
+        assertThat(actualResult, is(equalTo(expectedResult)));
+    }
+}
diff --git a/src/test/java/com/cisco/gerrit/plugins/slack/util/ResourceHelperTest.java b/src/test/java/com/cisco/gerrit/plugins/slack/util/ResourceHelperTest.java
new file mode 100644
index 0000000..3a9c046
--- /dev/null
+++ b/src/test/java/com/cisco/gerrit/plugins/slack/util/ResourceHelperTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2016 Cisco Systems, Inc.
+ *
+ * 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.cisco.gerrit.plugins.slack.util;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class ResourceHelperTest
+{
+    private static final String RESOURCE_NAME = "test.properties";
+
+    @Test
+    public void testLoadNamedResourceAsStream() throws Exception
+    {
+        assertNotNull(ResourceHelper.loadNamedResourceAsStream(RESOURCE_NAME));
+    }
+
+    @Test
+    public void testLoadNamedResourceAsString() throws Exception
+    {
+        String resource;
+        resource = ResourceHelper.loadNamedResourceAsString(RESOURCE_NAME);
+
+        assertNotNull(resource);
+        assertTrue(resource.length() > 0);
+    }
+}
diff --git a/src/test/resources/test.properties b/src/test/resources/test.properties
new file mode 100644
index 0000000..615036b
--- /dev/null
+++ b/src/test/resources/test.properties
@@ -0,0 +1,18 @@
+#
+# Copyright 2016 Cisco Systems, Inc.
+#
+# 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.
+#
+#
+
+webhook-url=https://hooks.slack.com/services/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