Merge branch 'stable-2.14' into stable-2.15

* stable-2.14:
  Upgrade bazlets to latest stable-2.14 to build with 2.14.21 API

Change-Id: Iae49e5d785928694f0031d8b30999358d8b896b4
diff --git a/README.md b/README.md
index 85661e4..2007a3d 100644
--- a/README.md
+++ b/README.md
@@ -98,6 +98,15 @@
         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).
+    ignore-unchanged-patch-set - boolean (true/false)
+        Whether a Slack notification about a new patch-set shouldn't be
+        published when code didn't change (defaults to true).
+    ignore-wip-patch-set - boolean (true/false)
+        Whether any Slack notifications regarding a work-in-progress change
+        shouldn't be published (defaults to true).
+    ignore-private-patch-set - boolean (true/false)
+        Whether any Slack notifications regarding a private change shouldn't
+        be published (defaults to true).
     publish-on-patch-set-created - boolean (true/false)
         Whether a Slack notification should be published when a new patch set
         is created.
@@ -110,7 +119,15 @@
     publish-on-reviewer-added - boolean (true/false)
         Whether a Slack notification should be published when a reviewer is
         added to a review.
-        
+    publish-on-wip-ready - boolean(true/false)
+        Whether a Slack notification should be published when a
+        work-in-progress change is marked ready (defaults to the value for
+        publish-on-patch-set-created).
+    publish-on-private-to-public - boolean(true/false)
+        Whether a Slack notification should be published when a
+        private change is changed to public (defaults to the value for
+        publish-on-patch-set-created).
+   
         
 Proxy Configuration
 -------------------
diff --git a/WORKSPACE b/WORKSPACE
index 5d340e8..62d5bbc 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -3,7 +3,7 @@
 load("//:bazlets.bzl", "load_bazlets")
 
 load_bazlets(
-    commit = "78c35a7eb33ee5ea0980923e246c7dba37347193",
+    commit = "f53f51fb660552d0581aa0ba52c3836ed63d56a3",
     #local_path = "/home/<user>/projects/bazlets"
 )
 
diff --git a/src/main/java/com/cisco/gerrit/plugins/slack/PublishEventListener.java b/src/main/java/com/cisco/gerrit/plugins/slack/PublishEventListener.java
index 6f862f5..f0988d1 100644
--- a/src/main/java/com/cisco/gerrit/plugins/slack/PublishEventListener.java
+++ b/src/main/java/com/cisco/gerrit/plugins/slack/PublishEventListener.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
 import com.google.gerrit.server.events.ReviewerAddedEvent;
+import com.google.gerrit.server.events.WorkInProgressStateChangedEvent;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import org.slf4j.Logger;
@@ -84,6 +85,13 @@
         config = new ProjectConfig(configFactory, reviewerAddedEvent.change.get().project);
 
         messageGenerator = MessageGeneratorFactory.newInstance(reviewerAddedEvent, config);
+      } else if (event instanceof WorkInProgressStateChangedEvent) {
+        WorkInProgressStateChangedEvent wipStateChangedEvent;
+        wipStateChangedEvent = (WorkInProgressStateChangedEvent) event;
+
+        config = new ProjectConfig(configFactory, wipStateChangedEvent.change.get().project);
+
+        messageGenerator = MessageGeneratorFactory.newInstance(wipStateChangedEvent, config);
       } else {
         LOGGER.debug("Event " + event + " not currently supported");
 
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
index 399f3ac..088cc4f 100644
--- a/src/main/java/com/cisco/gerrit/plugins/slack/config/ProjectConfig.java
+++ b/src/main/java/com/cisco/gerrit/plugins/slack/config/ProjectConfig.java
@@ -40,10 +40,15 @@
   private String channel;
   private String username;
   private String ignore;
+  private boolean ignoreUnchangedPatchSet;
+  private boolean ignoreWorkInProgressPatchSet;
+  private boolean ignorePrivatePatchSet;
   private boolean publishOnPatchSetCreated;
   private boolean publishOnChangeMerged;
   private boolean publishOnCommentAdded;
   private boolean publishOnReviewerAdded;
+  private boolean publishOnWipReady;
+  private boolean publishOnPrivateToPublic;
 
   private String proxyHost;
   private int proxyPort;
@@ -88,6 +93,21 @@
               .getFromProjectConfigWithInheritance(projectNameKey, CONFIG_NAME)
               .getString("ignore", "");
 
+      ignoreUnchangedPatchSet =
+          configFactory
+              .getFromProjectConfigWithInheritance(projectNameKey, CONFIG_NAME)
+              .getBoolean("ignore-unchanged-patch-set", true);
+
+      ignoreWorkInProgressPatchSet =
+          configFactory
+              .getFromProjectConfigWithInheritance(projectNameKey, CONFIG_NAME)
+              .getBoolean("ignore-wip-patch-set", true);
+
+      ignorePrivatePatchSet =
+          configFactory
+              .getFromProjectConfigWithInheritance(projectNameKey, CONFIG_NAME)
+              .getBoolean("ignore-private-patch-set", true);
+
       publishOnPatchSetCreated =
           configFactory
               .getFromProjectConfigWithInheritance(projectNameKey, CONFIG_NAME)
@@ -108,6 +128,16 @@
               .getFromProjectConfigWithInheritance(projectNameKey, CONFIG_NAME)
               .getBoolean("publish-on-reviewer-added", true);
 
+      publishOnWipReady =
+          configFactory
+              .getFromProjectConfigWithInheritance(projectNameKey, CONFIG_NAME)
+              .getBoolean("publish-on-wip-ready", publishOnPatchSetCreated);
+
+      publishOnPrivateToPublic =
+          configFactory
+              .getFromProjectConfigWithInheritance(projectNameKey, CONFIG_NAME)
+              .getBoolean("publish-on-private-to-public", publishOnPatchSetCreated);
+
       proxyHost = configFactory.getFromGerritConfig(CONFIG_NAME).getString("proxy-host", null);
 
       proxyPort = configFactory.getFromGerritConfig(CONFIG_NAME).getInt("proxy-port", 8080);
@@ -142,6 +172,18 @@
     return ignore;
   }
 
+  public boolean getIgnoreUnchangedPatchSet() {
+    return ignoreUnchangedPatchSet;
+  }
+
+  public boolean getIgnoreWorkInProgressPatchSet() {
+    return ignoreWorkInProgressPatchSet;
+  }
+
+  public boolean getIgnorePrivatePatchSet() {
+    return ignorePrivatePatchSet;
+  }
+
   public boolean shouldPublishOnPatchSetCreated() {
     return publishOnPatchSetCreated;
   }
@@ -158,6 +200,14 @@
     return publishOnReviewerAdded;
   }
 
+  public boolean shouldPublishOnWipReady() {
+    return publishOnWipReady;
+  }
+
+  public boolean shouldPublishOnPrivateToPublic() {
+    return publishOnPrivateToPublic;
+  }
+
   public String getProxyHost() {
     return proxyHost;
   }
diff --git a/src/main/java/com/cisco/gerrit/plugins/slack/message/CommentAddedMessageGenerator.java b/src/main/java/com/cisco/gerrit/plugins/slack/message/CommentAddedMessageGenerator.java
index 645ca29..2e2a1eb 100644
--- a/src/main/java/com/cisco/gerrit/plugins/slack/message/CommentAddedMessageGenerator.java
+++ b/src/main/java/com/cisco/gerrit/plugins/slack/message/CommentAddedMessageGenerator.java
@@ -20,6 +20,7 @@
 import static org.apache.commons.lang.StringUtils.substringBefore;
 
 import com.cisco.gerrit.plugins.slack.config.ProjectConfig;
+import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.events.CommentAddedEvent;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -54,7 +55,24 @@
 
   @Override
   public boolean shouldPublish() {
-    return config.isEnabled() && config.shouldPublishOnCommentAdded();
+    if (!config.isEnabled() || !config.shouldPublishOnCommentAdded()) {
+      return false;
+    }
+
+    try {
+      ChangeAttribute change;
+      change = event.change.get();
+      if (config.getIgnorePrivatePatchSet() && Boolean.TRUE.equals(change.isPrivate)) {
+        return false;
+      }
+      if (config.getIgnoreWorkInProgressPatchSet() && Boolean.TRUE.equals(change.wip)) {
+        return false;
+      }
+    } catch (Exception e) {
+      LOGGER.warn("Error checking private and work-in-progress status", e);
+    }
+
+    return true;
   }
 
   @Override
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
index f4dc367..c1c5c9a 100644
--- a/src/main/java/com/cisco/gerrit/plugins/slack/message/MessageGeneratorFactory.java
+++ b/src/main/java/com/cisco/gerrit/plugins/slack/message/MessageGeneratorFactory.java
@@ -22,7 +22,9 @@
 import com.google.gerrit.server.events.CommentAddedEvent;
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.gerrit.server.events.PrivateStateChangedEvent;
 import com.google.gerrit.server.events.ReviewerAddedEvent;
+import com.google.gerrit.server.events.WorkInProgressStateChangedEvent;
 
 /**
  * Factory used to create event specific MessageGenerator instances.
@@ -90,6 +92,37 @@
   }
 
   /**
+   * Creates a new MessageGenerator for work-in-progress state changed events.
+   *
+   * @param event A WorkInProgressStateChangedEvent instance
+   * @param config A ProjectConfig instance for the given event
+   * @return A MessageGenerator instance capable of generating a message for a
+   *     WorkInProgressStateChangedEvent.
+   */
+  public static MessageGenerator newInstance(
+      WorkInProgressStateChangedEvent event, ProjectConfig config) {
+    WorkInProgressStateChangedGenerator messageGenerator;
+    messageGenerator = new WorkInProgressStateChangedGenerator(event, config);
+
+    return messageGenerator;
+  }
+
+  /**
+   * Creates a new MessageGenerator for private state changed events.
+   *
+   * @param event A PrivateStateChangedEvent instance
+   * @param config A ProjectConfig instance for the given event
+   * @return A MessageGenerator instance capable of generating a message for a
+   *     PrivateStateChangedEvent.
+   */
+  public static MessageGenerator newInstance(PrivateStateChangedEvent event, ProjectConfig config) {
+    PrivateStateChangedGenerator messageGenerator;
+    messageGenerator = new PrivateStateChangedGenerator(event, config);
+
+    return messageGenerator;
+  }
+
+  /**
    * Creates a new MessageGenerator for unsupported events.
    *
    * @param event An Event instance
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
index 022feba..4aae685 100644
--- a/src/main/java/com/cisco/gerrit/plugins/slack/message/PatchSetCreatedMessageGenerator.java
+++ b/src/main/java/com/cisco/gerrit/plugins/slack/message/PatchSetCreatedMessageGenerator.java
@@ -20,6 +20,8 @@
 import static org.apache.commons.lang.StringUtils.substringBefore;
 
 import com.cisco.gerrit.plugins.slack.config.ProjectConfig;
+import com.google.gerrit.extensions.client.ChangeKind;
+import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -54,12 +56,53 @@
     this.config = config;
   }
 
+  private boolean unchangedChangeKind(ChangeKind kind) {
+    switch (kind) {
+      case TRIVIAL_REBASE:
+        return true;
+      case MERGE_FIRST_PARENT_UPDATE:
+        return true;
+      case NO_CODE_CHANGE:
+        return true;
+      case NO_CHANGE:
+        return true;
+      case REWORK:
+        return false;
+      default:
+        LOGGER.warn("Unknown ChangeKind", kind);
+    }
+    // Default unknown ChangeKind's to changed
+    return false;
+  }
+
   @Override
   public boolean shouldPublish() {
     if (!config.isEnabled() || !config.shouldPublishOnPatchSetCreated()) {
       return false;
     }
 
+    // Ignore rebases or no code changes
+    try {
+      if (config.getIgnoreUnchangedPatchSet() && unchangedChangeKind(event.patchSet.get().kind)) {
+        return false;
+      }
+    } catch (Exception e) {
+      LOGGER.warn("Error checking patch set kind", e);
+    }
+
+    try {
+      ChangeAttribute change;
+      change = event.change.get();
+      if (config.getIgnorePrivatePatchSet() && Boolean.TRUE.equals(change.isPrivate)) {
+        return false;
+      }
+      if (config.getIgnoreWorkInProgressPatchSet() && Boolean.TRUE.equals(change.wip)) {
+        return false;
+      }
+    } catch (Exception e) {
+      LOGGER.warn("Error checking private and work-in-progress status", e);
+    }
+
     boolean result;
     result = true;
 
diff --git a/src/main/java/com/cisco/gerrit/plugins/slack/message/PrivateStateChangedGenerator.java b/src/main/java/com/cisco/gerrit/plugins/slack/message/PrivateStateChangedGenerator.java
new file mode 100644
index 0000000..625729e
--- /dev/null
+++ b/src/main/java/com/cisco/gerrit/plugins/slack/message/PrivateStateChangedGenerator.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2017 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 static org.apache.commons.lang.StringUtils.substringBefore;
+
+import com.cisco.gerrit.plugins.slack.config.ProjectConfig;
+import com.google.gerrit.server.events.PrivateStateChangedEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A specific MessageGenerator implementation that can generate a message for a private state
+ * changed event.
+ *
+ * @author James Hartig
+ */
+public class PrivateStateChangedGenerator implements MessageGenerator {
+  /** The class logger instance. */
+  private static final Logger LOGGER = LoggerFactory.getLogger(PrivateStateChangedGenerator.class);
+
+  private ProjectConfig config;
+  private PrivateStateChangedEvent event;
+
+  /**
+   * Creates a new PrivateStateChangedGenerator instance using the provided PrivateStateChangedEvent
+   * instance.
+   *
+   * @param event The PrivateStateChangedEvent instance to generate a message for.
+   */
+  PrivateStateChangedGenerator(PrivateStateChangedEvent event, ProjectConfig config) {
+    if (event == null) {
+      throw new NullPointerException("event cannot be null");
+    }
+
+    this.event = event;
+    this.config = config;
+  }
+
+  @Override
+  public boolean shouldPublish() {
+    if (!config.isEnabled() || !config.shouldPublishOnPrivateToPublic()) {
+      return false;
+    }
+
+    // If the change is still private then ignore
+    if (Boolean.TRUE.equals(event.change.get().isPrivate)) {
+      return false;
+    }
+    return true;
+  }
+
+  @Override
+  public String generate() {
+    String message;
+    message = "";
+
+    try {
+      MessageTemplate template;
+      template = new MessageTemplate();
+
+      template.setChannel(config.getChannel());
+      template.setName(event.changer.get().name);
+      template.setAction("proposed");
+      template.setNumber(event.change.get().number);
+      template.setProject(event.change.get().project);
+      template.setBranch(event.change.get().branch);
+      template.setUrl(event.change.get().url);
+      template.setTitle(substringBefore(event.change.get().commitMessage, "\n"));
+
+      message = template.render();
+    } catch (Exception e) {
+      LOGGER.error("Error generating message: " + e.getMessage(), e);
+    }
+
+    return message;
+  }
+}
diff --git a/src/main/java/com/cisco/gerrit/plugins/slack/message/ReviewerAddedMessageGenerator.java b/src/main/java/com/cisco/gerrit/plugins/slack/message/ReviewerAddedMessageGenerator.java
index 192517a..2a42c58 100644
--- a/src/main/java/com/cisco/gerrit/plugins/slack/message/ReviewerAddedMessageGenerator.java
+++ b/src/main/java/com/cisco/gerrit/plugins/slack/message/ReviewerAddedMessageGenerator.java
@@ -20,6 +20,7 @@
 import static org.apache.commons.lang.StringUtils.substringBefore;
 
 import com.cisco.gerrit.plugins.slack.config.ProjectConfig;
+import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.events.ReviewerAddedEvent;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -54,7 +55,24 @@
 
   @Override
   public boolean shouldPublish() {
-    return config.isEnabled() && config.shouldPublishOnReviewerAdded();
+    if (!config.isEnabled() || !config.shouldPublishOnReviewerAdded()) {
+      return false;
+    }
+
+    try {
+      ChangeAttribute change;
+      change = event.change.get();
+      if (config.getIgnorePrivatePatchSet() && Boolean.TRUE.equals(change.isPrivate)) {
+        return false;
+      }
+      if (config.getIgnoreWorkInProgressPatchSet() && Boolean.TRUE.equals(change.wip)) {
+        return false;
+      }
+    } catch (Exception e) {
+      LOGGER.warn("Error checking private and work-in-progress status", e);
+    }
+
+    return true;
   }
 
   @Override
diff --git a/src/main/java/com/cisco/gerrit/plugins/slack/message/WorkInProgressStateChangedGenerator.java b/src/main/java/com/cisco/gerrit/plugins/slack/message/WorkInProgressStateChangedGenerator.java
new file mode 100644
index 0000000..a5ec1da
--- /dev/null
+++ b/src/main/java/com/cisco/gerrit/plugins/slack/message/WorkInProgressStateChangedGenerator.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2017 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 static org.apache.commons.lang.StringUtils.substringBefore;
+
+import com.cisco.gerrit.plugins.slack.config.ProjectConfig;
+import com.google.gerrit.server.events.WorkInProgressStateChangedEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A specific MessageGenerator implementation that can generate a message for a work-in-progress
+ * state changed event.
+ *
+ * @author James Hartig
+ */
+public class WorkInProgressStateChangedGenerator implements MessageGenerator {
+  /** The class logger instance. */
+  private static final Logger LOGGER =
+      LoggerFactory.getLogger(WorkInProgressStateChangedGenerator.class);
+
+  private ProjectConfig config;
+  private WorkInProgressStateChangedEvent event;
+
+  /**
+   * Creates a new WorkInProgressStateChangedGenerator instance using the provided
+   * WorkInProgressStateChangedEvent instance.
+   *
+   * @param event The WorkInProgressStateChangedEvent instance to generate a message for.
+   */
+  WorkInProgressStateChangedGenerator(WorkInProgressStateChangedEvent event, ProjectConfig config) {
+    if (event == null) {
+      throw new NullPointerException("event cannot be null");
+    }
+
+    this.event = event;
+    this.config = config;
+  }
+
+  @Override
+  public boolean shouldPublish() {
+    if (!config.isEnabled() || !config.shouldPublishOnWipReady()) {
+      return false;
+    }
+
+    // If the change is still work-in-progress then ignore
+    if (Boolean.TRUE.equals(event.change.get().wip)) {
+      return false;
+    }
+    return true;
+  }
+
+  @Override
+  public String generate() {
+    String message;
+    message = "";
+
+    try {
+      MessageTemplate template;
+      template = new MessageTemplate();
+
+      template.setChannel(config.getChannel());
+      template.setName(event.changer.get().name);
+      template.setAction("proposed");
+      template.setNumber(event.change.get().number);
+      template.setProject(event.change.get().project);
+      template.setBranch(event.change.get().branch);
+      template.setUrl(event.change.get().url);
+      template.setTitle(substringBefore(event.change.get().commitMessage, "\n"));
+
+      message = template.render();
+    } catch (Exception e) {
+      LOGGER.error("Error generating message: " + e.getMessage(), e);
+    }
+
+    return message;
+  }
+}
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
index e01b009..303b7f3 100644
--- a/src/test/java/com/cisco/gerrit/plugins/slack/message/ChangeMergedMessageGeneratorTest.java
+++ b/src/test/java/com/cisco/gerrit/plugins/slack/message/ChangeMergedMessageGeneratorTest.java
@@ -154,6 +154,34 @@
   }
 
   @Test
+  public void publishesWhenWorkInProgress() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig();
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockChange.wip = true;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(true));
+  }
+
+  @Test
+  public void publishesWhenPrivate() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig();
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockChange.isPrivate = true;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(true));
+  }
+
+  @Test
   public void generatesExpectedMessage() throws Exception {
     // Setup mocks
     ProjectConfig config = getConfig();
diff --git a/src/test/java/com/cisco/gerrit/plugins/slack/message/CommentAddedMessageGeneratorTest.java b/src/test/java/com/cisco/gerrit/plugins/slack/message/CommentAddedMessageGeneratorTest.java
index 5ca36ae..159993a 100644
--- a/src/test/java/com/cisco/gerrit/plugins/slack/message/CommentAddedMessageGeneratorTest.java
+++ b/src/test/java/com/cisco/gerrit/plugins/slack/message/CommentAddedMessageGeneratorTest.java
@@ -64,7 +64,11 @@
     when(Project.NameKey.parse(PROJECT_NAME)).thenReturn(mockNameKey);
   }
 
-  private ProjectConfig getConfig(boolean publishOnCommentAdded) throws Exception {
+  private ProjectConfig getConfig(
+      boolean publishOnCommentAdded,
+      boolean ignoreWorkInProgressPatchSet,
+      boolean ignorePrivatePatchSet)
+      throws Exception {
     Project.NameKey projectNameKey;
     projectNameKey = Project.NameKey.parse(PROJECT_NAME);
 
@@ -83,12 +87,26 @@
     when(mockPluginConfig.getString("ignore", "")).thenReturn("^WIP.*");
     when(mockPluginConfig.getBoolean("publish-on-comment-added", true))
         .thenReturn(publishOnCommentAdded);
+    when(mockPluginConfig.getBoolean("ignore-wip-patch-set", true))
+        .thenReturn(ignoreWorkInProgressPatchSet);
+    when(mockPluginConfig.getBoolean("ignore-private-patch-set", true))
+        .thenReturn(ignorePrivatePatchSet);
 
     return new ProjectConfig(mockConfigFactory, PROJECT_NAME);
   }
 
   private ProjectConfig getConfig() throws Exception {
-    return getConfig(true /* publishOnCommentAdded */);
+    return getConfig(
+        true /* publishOnCommentAdded */,
+        true /* ignoreWorkInProgressPatchSet */,
+        true /* ignorePrivatePatchSet */);
+  }
+
+  private ProjectConfig getConfig(boolean publishOnCommentAdded) throws Exception {
+    return getConfig(
+        publishOnCommentAdded,
+        true /* ignoreWorkInProgressPatchSet */,
+        true /* ignorePrivatePatchSet */);
   }
 
   @Test
@@ -152,6 +170,74 @@
   }
 
   @Test
+  public void doesNotPublishWhenWorkInProgress() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig();
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockChange.isPrivate = false;
+    mockChange.wip = true;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(false));
+  }
+
+  @Test
+  public void doesNotPublishWhenPrivate() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig();
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockChange.isPrivate = true;
+    mockChange.wip = false;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(false));
+  }
+
+  @Test
+  public void publishesWhenWorkInProgress() throws Exception {
+    // Setup mocks
+    ProjectConfig config =
+        getConfig(
+            true /* publishOnCommentAdded */,
+            false /* ignoreWorkInProgressPatchSet */,
+            false /* ignorePrivatePatchSet */);
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockChange.isPrivate = false;
+    mockChange.wip = true;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(true));
+  }
+
+  @Test
+  public void publishesWhenPrivate() throws Exception {
+    // Setup mocks
+    ProjectConfig config =
+        getConfig(
+            true /* publishOnCommentAdded */,
+            false /* ignoreWorkInProgressPatchSet */,
+            false /* ignorePrivatePatchSet */);
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockChange.isPrivate = true;
+    mockChange.wip = false;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(true));
+  }
+
+  @Test
   public void generatesExpectedMessage() throws Exception {
     // Setup mocks
     ProjectConfig config = getConfig();
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
index d534d8e..64cbc70 100644
--- a/src/test/java/com/cisco/gerrit/plugins/slack/message/PatchSetCreatedMessageGeneratorTest.java
+++ b/src/test/java/com/cisco/gerrit/plugins/slack/message/PatchSetCreatedMessageGeneratorTest.java
@@ -25,11 +25,13 @@
 
 import com.cisco.gerrit.plugins.slack.config.ProjectConfig;
 import com.google.common.base.Suppliers;
+import com.google.gerrit.extensions.client.ChangeKind;
 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.data.PatchSetAttribute;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
 import org.junit.Before;
 import org.junit.Test;
@@ -53,6 +55,7 @@
   private PatchSetCreatedEvent mockEvent = mock(PatchSetCreatedEvent.class);
   private AccountAttribute mockAccount = mock(AccountAttribute.class);
   private ChangeAttribute mockChange = mock(ChangeAttribute.class);
+  private PatchSetAttribute mockPatchSet = mock(PatchSetAttribute.class);
 
   @Before
   public void setup() throws Exception {
@@ -60,7 +63,12 @@
     when(Project.NameKey.parse(PROJECT_NAME)).thenReturn(mockNameKey);
   }
 
-  private ProjectConfig getConfig(String ignore, boolean publishOnPatchSetCreated)
+  private ProjectConfig getConfig(
+      String ignore,
+      boolean publishOnPatchSetCreated,
+      boolean ignoreUnchangedPatchSet,
+      boolean ignoreWorkInProgressPatchSet,
+      boolean ignorePrivatePatchSet)
       throws Exception {
     Project.NameKey projectNameKey;
     projectNameKey = Project.NameKey.parse(PROJECT_NAME);
@@ -80,20 +88,65 @@
     when(mockPluginConfig.getString("ignore", "")).thenReturn(ignore);
     when(mockPluginConfig.getBoolean("publish-on-patch-set-created", true))
         .thenReturn(publishOnPatchSetCreated);
+    when(mockPluginConfig.getBoolean("ignore-unchanged-patch-set", true))
+        .thenReturn(ignoreUnchangedPatchSet);
+    when(mockPluginConfig.getBoolean("ignore-wip-patch-set", true))
+        .thenReturn(ignoreWorkInProgressPatchSet);
+    when(mockPluginConfig.getBoolean("ignore-private-patch-set", true))
+        .thenReturn(ignorePrivatePatchSet);
 
     return new ProjectConfig(mockConfigFactory, PROJECT_NAME);
   }
 
   private ProjectConfig getConfig(String ignore) throws Exception {
-    return getConfig(ignore, true /* publishOnPatchSetCreated */);
+    return getConfig(
+        ignore,
+        true /* publishOnPatchSetCreated */,
+        true /* ignoreUnchangedPatchSet */,
+        true /* ignoreWorkInProgressPatchSet */,
+        true /* ignorePrivatePatchSet */);
   }
 
   private ProjectConfig getConfig(boolean publishOnPatchSetCreated) throws Exception {
-    return getConfig("^WIP.*", publishOnPatchSetCreated);
+    return getConfig(
+        "^WIP.*",
+        publishOnPatchSetCreated,
+        true /* ignoreUnchangedPatchSet */,
+        true /* ignoreWorkInProgressPatchSet */,
+        true /* ignorePrivatePatchSet */);
+  }
+
+  private ProjectConfig getConfig(boolean publishOnPatchSetCreated, boolean ignoreUnchangedPatchSet)
+      throws Exception {
+    return getConfig(
+        "^WIP.*",
+        publishOnPatchSetCreated,
+        ignoreUnchangedPatchSet,
+        true /* ignoreWorkInProgressPatchSet */,
+        true /* ignorePrivatePatchSet */);
+  }
+
+  private ProjectConfig getConfig(
+      boolean publishOnPatchSetCreated,
+      boolean ignoreUnchangedPatchSet,
+      boolean ignoreWorkInProgressPatchSet,
+      boolean ignorePrivatePatchSet)
+      throws Exception {
+    return getConfig(
+        "^WIP.*",
+        publishOnPatchSetCreated,
+        ignoreUnchangedPatchSet,
+        ignoreWorkInProgressPatchSet,
+        ignorePrivatePatchSet);
   }
 
   private ProjectConfig getConfig() throws Exception {
-    return getConfig("^WIP.*", true /* publishOnPatchSetCreated */);
+    return getConfig(
+        "^WIP.*",
+        true /* publishOnPatchSetCreated */,
+        true /* ignoreUnchangedPatchSet */,
+        true /* ignoreWorkInProgressPatchSet */,
+        true /* ignorePrivatePatchSet */);
   }
 
   @Test
@@ -159,6 +212,207 @@
   }
 
   @Test
+  public void doesNotPublishWhenTrivialRebase() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig();
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockEvent.patchSet = Suppliers.ofInstance(mockPatchSet);
+    mockPatchSet.kind = ChangeKind.TRIVIAL_REBASE;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(false));
+  }
+
+  @Test
+  public void publishesWhenTrivialRebase() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig(true, false);
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockEvent.patchSet = Suppliers.ofInstance(mockPatchSet);
+    mockPatchSet.kind = ChangeKind.TRIVIAL_REBASE;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(true));
+  }
+
+  @Test
+  public void doesNotPublishWhenMergeUpdate() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig();
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockEvent.patchSet = Suppliers.ofInstance(mockPatchSet);
+    mockPatchSet.kind = ChangeKind.MERGE_FIRST_PARENT_UPDATE;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(false));
+  }
+
+  @Test
+  public void publishesWhenMergeUpdate() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig(true, false);
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockEvent.patchSet = Suppliers.ofInstance(mockPatchSet);
+    mockPatchSet.kind = ChangeKind.MERGE_FIRST_PARENT_UPDATE;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(true));
+  }
+
+  @Test
+  public void doesNotPublishWhenNoCodeChange() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig();
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockEvent.patchSet = Suppliers.ofInstance(mockPatchSet);
+    mockPatchSet.kind = ChangeKind.NO_CODE_CHANGE;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(false));
+  }
+
+  @Test
+  public void publishesWhenNoCodeChange() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig(true, false);
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockEvent.patchSet = Suppliers.ofInstance(mockPatchSet);
+    mockPatchSet.kind = ChangeKind.NO_CODE_CHANGE;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(true));
+  }
+
+  @Test
+  public void doesNotPublishWhenNoChange() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig();
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockEvent.patchSet = Suppliers.ofInstance(mockPatchSet);
+    mockPatchSet.kind = ChangeKind.NO_CHANGE;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(false));
+  }
+
+  @Test
+  public void publishesWhenNoChange() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig(true, false);
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockEvent.patchSet = Suppliers.ofInstance(mockPatchSet);
+    mockPatchSet.kind = ChangeKind.NO_CHANGE;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(true));
+  }
+
+  @Test
+  public void publishesWhenRework() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig();
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockEvent.patchSet = Suppliers.ofInstance(mockPatchSet);
+    mockPatchSet.kind = ChangeKind.REWORK;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(true));
+  }
+
+  @Test
+  public void doesNotPublishWhenWorkInProgress() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig();
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockChange.wip = true;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(false));
+  }
+
+  @Test
+  public void doesNotPublishWhenPrivate() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig();
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockChange.isPrivate = true;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(false));
+  }
+
+  @Test
+  public void publishesWhenWorkInProgress() throws Exception {
+    // Setup mocks
+    ProjectConfig config =
+        getConfig(
+            true /* publishOnPatchSetCreated */,
+            false /* ignoreRebaseEmptyPatchSet */,
+            false /* ignoreWorkInProgressPatchSet */,
+            false /* ignorePrivatePatchSet */);
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockChange.wip = true;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(true));
+  }
+
+  @Test
+  public void publishesWhenPrivate() throws Exception {
+    // Setup mocks
+    ProjectConfig config =
+        getConfig(
+            true /* publishOnPatchSetCreated */,
+            false /* ignoreRebaseEmptyPatchSet */,
+            false /* ignoreWorkInProgressPatchSet */,
+            false /* ignorePrivatePatchSet */);
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockChange.isPrivate = true;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(true));
+  }
+
+  @Test
   public void generatesExpectedMessage() throws Exception {
     // Setup mocks
     ProjectConfig config = getConfig();
diff --git a/src/test/java/com/cisco/gerrit/plugins/slack/message/PrivateStateChangedGeneratorTest.java b/src/test/java/com/cisco/gerrit/plugins/slack/message/PrivateStateChangedGeneratorTest.java
new file mode 100644
index 0000000..c9684cb
--- /dev/null
+++ b/src/test/java/com/cisco/gerrit/plugins/slack/message/PrivateStateChangedGeneratorTest.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2017 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 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;
+
+import com.cisco.gerrit.plugins.slack.config.ProjectConfig;
+import com.google.common.base.Suppliers;
+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.PrivateStateChangedEvent;
+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;
+
+/**
+ * Tests for the PrivateStateChangedGenerator class. The expected behavior is that the
+ * PrivateStateChangedGenerator should publish when the state changes.
+ */
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({Project.NameKey.class})
+public class PrivateStateChangedGeneratorTest {
+  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 PrivateStateChangedEvent mockEvent = mock(PrivateStateChangedEvent.class);
+  private AccountAttribute mockAccount = mock(AccountAttribute.class);
+  private ChangeAttribute mockChange = mock(ChangeAttribute.class);
+
+  @Before
+  public void setup() throws Exception {
+    PowerMockito.mockStatic(Project.NameKey.class);
+    when(Project.NameKey.parse(PROJECT_NAME)).thenReturn(mockNameKey);
+  }
+
+  private ProjectConfig getConfig(boolean publishOnPrivateToPublic) throws Exception {
+    Project.NameKey projectNameKey;
+    projectNameKey = Project.NameKey.parse(PROJECT_NAME);
+
+    // Setup mocks
+    when(mockConfigFactory.getFromProjectConfigWithInheritance(
+            projectNameKey, ProjectConfig.CONFIG_NAME))
+        .thenReturn(mockPluginConfig);
+
+    when(mockConfigFactory.getFromGerritConfig(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.*");
+    when(mockPluginConfig.getBoolean("publish-on-patch-set-created", true)).thenReturn(true);
+    when(mockPluginConfig.getBoolean("publish-on-private-to-public", true))
+        .thenReturn(publishOnPrivateToPublic);
+
+    return new ProjectConfig(mockConfigFactory, PROJECT_NAME);
+  }
+
+  private ProjectConfig getConfig() throws Exception {
+    return getConfig(true /* publishOnPrivateToPublic */);
+  }
+
+  @Test
+  public void factoryCreatesExpectedType() throws Exception {
+    ProjectConfig config = getConfig();
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator instanceof PrivateStateChangedGenerator, is(true));
+  }
+
+  @Test
+  public void publishesWhenExpected() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig();
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(true));
+  }
+
+  @Test
+  public void doesNotPublishWhenTurnedOff() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig(false /* publishOnPrivateToPublic */);
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(false));
+  }
+
+  @Test
+  public void doesNotPublishWhenPrivate() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig();
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockChange.isPrivate = true;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(false));
+  }
+
+  @Test
+  public void generatesExpectedMessage() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig();
+    mockEvent.changer = Suppliers.ofInstance(mockAccount);
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+
+    mockChange.number = 1234;
+    mockChange.project = "testproject";
+    mockChange.branch = "master";
+    mockChange.url = "https://change/";
+    mockChange.commitMessage = "This is the title\nThis is the message body.";
+
+    mockAccount.name = "Unit Tester";
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    String expectedResult;
+    expectedResult =
+        "{\n"
+            + "  \"channel\": \"#testchannel\",\n"
+            + "  \"attachments\": [\n"
+            + "    {\n"
+            + "      \"fallback\": \"Unit Tester proposed testproject (master) https://change/: This is the title\",\n"
+            + "      \"pretext\": \"Unit Tester proposed <https://change/|testproject (master) change 1234>\",\n"
+            + "      \"title\": \"This is the title\",\n"
+            + "      \"title_link\": \"https://change/\",\n"
+            + "      \"text\": \"\",\n"
+            + "      \"color\": \"good\"\n"
+            + "    }\n"
+            + "  ]\n"
+            + "}\n";
+
+    String actualResult;
+    actualResult = messageGenerator.generate();
+
+    assertThat(actualResult, is(equalTo(expectedResult)));
+  }
+}
diff --git a/src/test/java/com/cisco/gerrit/plugins/slack/message/ReviewerAddedMessageGeneratorTest.java b/src/test/java/com/cisco/gerrit/plugins/slack/message/ReviewerAddedMessageGeneratorTest.java
index 9c1e716..c41c92e 100644
--- a/src/test/java/com/cisco/gerrit/plugins/slack/message/ReviewerAddedMessageGeneratorTest.java
+++ b/src/test/java/com/cisco/gerrit/plugins/slack/message/ReviewerAddedMessageGeneratorTest.java
@@ -64,7 +64,11 @@
     when(Project.NameKey.parse(PROJECT_NAME)).thenReturn(mockNameKey);
   }
 
-  private ProjectConfig getConfig(boolean publishOnReviewerAdded) throws Exception {
+  private ProjectConfig getConfig(
+      boolean publishOnReviewerAdded,
+      boolean ignoreWorkInProgressPatchSet,
+      boolean ignorePrivatePatchSet)
+      throws Exception {
     Project.NameKey projectNameKey;
     projectNameKey = Project.NameKey.parse(PROJECT_NAME);
 
@@ -83,12 +87,26 @@
     when(mockPluginConfig.getString("ignore", "")).thenReturn("^WIP.*");
     when(mockPluginConfig.getBoolean("publish-on-reviewer-added", true))
         .thenReturn(publishOnReviewerAdded);
+    when(mockPluginConfig.getBoolean("ignore-wip-patch-set", true))
+        .thenReturn(ignoreWorkInProgressPatchSet);
+    when(mockPluginConfig.getBoolean("ignore-private-patch-set", true))
+        .thenReturn(ignorePrivatePatchSet);
 
     return new ProjectConfig(mockConfigFactory, PROJECT_NAME);
   }
 
   private ProjectConfig getConfig() throws Exception {
-    return getConfig(true /* publishOnReviewerAdded */);
+    return getConfig(
+        true /* publishOnReviewerAdded */,
+        true /* ignoreWorkInProgressPatchSet */,
+        true /* ignorePrivatePatchSet */);
+  }
+
+  private ProjectConfig getConfig(boolean publishOnReviewerAdded) throws Exception {
+    return getConfig(
+        publishOnReviewerAdded,
+        true /* ignoreWorkInProgressPatchSet */,
+        true /* ignorePrivatePatchSet */);
   }
 
   @Test
@@ -137,6 +155,70 @@
   }
 
   @Test
+  public void doesNotPublishWhenWorkInProgress() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig();
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockChange.wip = true;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(false));
+  }
+
+  @Test
+  public void doesNotPublishWhenPrivate() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig();
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockChange.isPrivate = true;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(false));
+  }
+
+  @Test
+  public void publishesWhenWorkInProgress() throws Exception {
+    // Setup mocks
+    ProjectConfig config =
+        getConfig(
+            true /* publishOnReviewerAdded */,
+            false /* ignoreWorkInProgressPatchSet */,
+            false /* ignorePrivatePatchSet */);
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockChange.wip = true;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(true));
+  }
+
+  @Test
+  public void publishesWhenPrivate() throws Exception {
+    // Setup mocks
+    ProjectConfig config =
+        getConfig(
+            true /* publishOnReviewerAdded */,
+            false /* ignoreWorkInProgressPatchSet */,
+            false /* ignorePrivatePatchSet */);
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockChange.isPrivate = true;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(true));
+  }
+
+  @Test
   public void generatesExpectedMessage() throws Exception {
     // Setup mocks
     ProjectConfig config = getConfig();
diff --git a/src/test/java/com/cisco/gerrit/plugins/slack/message/WorkInProgressStateChangedGeneratorTest.java b/src/test/java/com/cisco/gerrit/plugins/slack/message/WorkInProgressStateChangedGeneratorTest.java
new file mode 100644
index 0000000..33accac
--- /dev/null
+++ b/src/test/java/com/cisco/gerrit/plugins/slack/message/WorkInProgressStateChangedGeneratorTest.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2017 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 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;
+
+import com.cisco.gerrit.plugins.slack.config.ProjectConfig;
+import com.google.common.base.Suppliers;
+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.WorkInProgressStateChangedEvent;
+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;
+
+/**
+ * Tests for the WorkInProgressStateChangedGenerator class. The expected behavior is that the
+ * WorkInProgressStateChangedGenerator should publish when the state changes.
+ */
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({Project.NameKey.class})
+public class WorkInProgressStateChangedGeneratorTest {
+  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 WorkInProgressStateChangedEvent mockEvent = mock(WorkInProgressStateChangedEvent.class);
+  private AccountAttribute mockAccount = mock(AccountAttribute.class);
+  private ChangeAttribute mockChange = mock(ChangeAttribute.class);
+
+  @Before
+  public void setup() throws Exception {
+    PowerMockito.mockStatic(Project.NameKey.class);
+    when(Project.NameKey.parse(PROJECT_NAME)).thenReturn(mockNameKey);
+  }
+
+  private ProjectConfig getConfig(boolean publishOnWipReady) throws Exception {
+    Project.NameKey projectNameKey;
+    projectNameKey = Project.NameKey.parse(PROJECT_NAME);
+
+    // Setup mocks
+    when(mockConfigFactory.getFromProjectConfigWithInheritance(
+            projectNameKey, ProjectConfig.CONFIG_NAME))
+        .thenReturn(mockPluginConfig);
+
+    when(mockConfigFactory.getFromGerritConfig(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.*");
+    when(mockPluginConfig.getBoolean("publish-on-patch-set-created", true)).thenReturn(true);
+    when(mockPluginConfig.getBoolean("publish-on-wip-ready", true)).thenReturn(publishOnWipReady);
+
+    return new ProjectConfig(mockConfigFactory, PROJECT_NAME);
+  }
+
+  private ProjectConfig getConfig() throws Exception {
+    return getConfig(true /* publishOnWipReady */);
+  }
+
+  @Test
+  public void factoryCreatesExpectedType() throws Exception {
+    ProjectConfig config = getConfig();
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator instanceof WorkInProgressStateChangedGenerator, is(true));
+  }
+
+  @Test
+  public void publishesWhenExpected() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig();
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(true));
+  }
+
+  @Test
+  public void doesNotPublishWhenTurnedOff() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig(false /* publishOnWipReady */);
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(false));
+  }
+
+  @Test
+  public void doesNotPublishWhenWorkInProgress() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig();
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+    mockChange.wip = true;
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    assertThat(messageGenerator.shouldPublish(), is(false));
+  }
+
+  @Test
+  public void generatesExpectedMessage() throws Exception {
+    // Setup mocks
+    ProjectConfig config = getConfig();
+    mockEvent.changer = Suppliers.ofInstance(mockAccount);
+    mockEvent.change = Suppliers.ofInstance(mockChange);
+
+    mockChange.number = 1234;
+    mockChange.project = "testproject";
+    mockChange.branch = "master";
+    mockChange.url = "https://change/";
+    mockChange.commitMessage = "This is the title\nThis is the message body.";
+
+    mockAccount.name = "Unit Tester";
+
+    // Test
+    MessageGenerator messageGenerator;
+    messageGenerator = MessageGeneratorFactory.newInstance(mockEvent, config);
+
+    String expectedResult;
+    expectedResult =
+        "{\n"
+            + "  \"channel\": \"#testchannel\",\n"
+            + "  \"attachments\": [\n"
+            + "    {\n"
+            + "      \"fallback\": \"Unit Tester proposed testproject (master) https://change/: This is the title\",\n"
+            + "      \"pretext\": \"Unit Tester proposed <https://change/|testproject (master) change 1234>\",\n"
+            + "      \"title\": \"This is the title\",\n"
+            + "      \"title_link\": \"https://change/\",\n"
+            + "      \"text\": \"\",\n"
+            + "      \"color\": \"good\"\n"
+            + "    }\n"
+            + "  ]\n"
+            + "}\n";
+
+    String actualResult;
+    actualResult = messageGenerator.generate();
+
+    assertThat(actualResult, is(equalTo(expectedResult)));
+  }
+}