Initial version of evict-cache plugin

This plugin allows to synchronize the eviction of caches between two
Gerrit instances sharing the same repositories and database.

The plugin needs to be installed in both instances and every time a
cache eviction occurs in one of the instances, the other instance's
cache is updated. This way, both caches are kept synchronized.

Synchronized caches are:
  - accounts
  - accounts_byemail
  - accounts_byname
  - adv_bases
  - groups
  - groups_byinclude
  - groups_byname
  - groups_byuuid
  - groups_external
  - groups_members
  - ldap_groups
  - ldap_usernames
  - project_list
  - projects
  - sshkeys
  - web_sessions

Other caches are not synchronized because their cached objects are
immutable.

Change-Id: I4f5c1dc5c705854ae6af798f392c681078355a6c
diff --git a/.buckconfig b/.buckconfig
new file mode 100644
index 0000000..4c18a27
--- /dev/null
+++ b/.buckconfig
@@ -0,0 +1,14 @@
+[alias]
+  evict-cache = //:evict-cache
+  plugin = //:evict-cache
+  src = //:evict-cache-sources
+
+[java]
+  src_roots = java, resources
+
+[project]
+  ignore = .git
+
+[cache]
+  mode = dir
+  dir = buck-out/cache
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b37f9a6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+/.buckd
+/.buckversion
+/.classpath
+/.project
+/.settings/
+/.watchmanconfig
+/buck-out/
+/bucklets
+/eclipse-out
diff --git a/BUCK b/BUCK
new file mode 100644
index 0000000..c15e58e
--- /dev/null
+++ b/BUCK
@@ -0,0 +1,61 @@
+include_defs('//bucklets/gerrit_plugin.bucklet')
+include_defs('//bucklets/java_sources.bucklet')
+include_defs('//bucklets/maven_jar.bucklet')
+
+SOURCES = glob(['src/main/java/**/*.java'])
+RESOURCES = glob(['src/main/resources/**/*'])
+
+DEPS = [
+  ':wiremock',
+]
+
+PROVIDED_DEPS = GERRIT_TESTS + [
+  '//lib:gson',
+]
+
+TEST_DEPS = GERRIT_PLUGIN_API + PROVIDED_DEPS + DEPS + [
+  ':evict-cache__plugin',
+]
+
+gerrit_plugin(
+  name = 'evict-cache',
+  srcs = SOURCES,
+  resources = RESOURCES,
+  manifest_entries = [
+    'Gerrit-PluginName: evict-cache',
+    'Gerrit-ApiType: plugin',
+    'Gerrit-Module: com.ericsson.gerrit.plugins.evictcache.Module',
+    'Gerrit-HttpModule: com.ericsson.gerrit.plugins.evictcache.HttpModule',
+    'Implementation-Title: evict-cache plugin',
+    'Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/evict-cache',
+    'Implementation-Vendor: Ericsson',
+  ],
+  provided_deps = PROVIDED_DEPS,
+  deps = DEPS,
+)
+
+java_sources(
+  name = 'evict-cache-sources',
+  srcs = SOURCES + RESOURCES,
+)
+
+java_library(
+  name = 'classpath',
+  deps = TEST_DEPS,
+)
+
+java_test(
+  name = 'evict-cache_tests',
+  srcs = glob(['src/test/java/**/*.java']),
+  labels = ['evict-cache'],
+  source_under_test = [':evict-cache__plugin'],
+  deps = TEST_DEPS,
+)
+
+maven_jar(
+  name = 'wiremock',
+  id = 'com.github.tomakehurst:wiremock:1.58:standalone',
+  sha1 = '21c8386a95c5dc54a9c55839c5a95083e42412ae',
+  license = 'Apache2.0',
+  attach_source = False,
+)
diff --git a/lib/BUCK b/lib/BUCK
new file mode 100644
index 0000000..8892994
--- /dev/null
+++ b/lib/BUCK
@@ -0,0 +1,8 @@
+include_defs('//bucklets/maven_jar.bucklet')
+
+maven_jar(
+  name = 'gson',
+  id = 'com.google.code.gson:gson:2.1',
+  sha1 = '2e66da15851f9f5b5079228f856c2f090ba98c38',
+  license = 'Apache2.0',
+)
diff --git a/lib/gerrit/BUCK b/lib/gerrit/BUCK
new file mode 100644
index 0000000..fe8119e
--- /dev/null
+++ b/lib/gerrit/BUCK
@@ -0,0 +1,20 @@
+include_defs('//bucklets/maven_jar.bucklet')
+
+VER = '2.13-SNAPSHOT'
+REPO = MAVEN_LOCAL
+
+maven_jar(
+  name = 'plugin-api',
+  id = 'com.google.gerrit:gerrit-plugin-api:' + VER,
+  license = 'Apache2.0',
+  attach_source = False,
+  repository = REPO,
+)
+
+maven_jar(
+  name = 'acceptance-framework',
+  id = 'com.google.gerrit:gerrit-acceptance-framework:' + VER,
+  license = 'Apache2.0',
+  attach_source = False,
+  repository = REPO,
+)
diff --git a/src/main/java/com/ericsson/gerrit/plugins/evictcache/CacheResponseHandler.java b/src/main/java/com/ericsson/gerrit/plugins/evictcache/CacheResponseHandler.java
new file mode 100644
index 0000000..768ee72
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/evictcache/CacheResponseHandler.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
+
+import com.ericsson.gerrit.plugins.evictcache.CacheResponseHandler.CacheResult;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+class CacheResponseHandler implements ResponseHandler<CacheResult> {
+
+  static class CacheResult {
+    private boolean successful;
+    private String message;
+
+    CacheResult(boolean successful, String message) {
+      this.successful = successful;
+      this.message = message;
+    }
+
+    boolean isSuccessful() {
+      return successful;
+    }
+
+    String getMessage() {
+      return message;
+    }
+  }
+
+  private static final Logger log =
+      LoggerFactory.getLogger(CacheResponseHandler.class);
+
+  @Override
+  public CacheResult handleResponse(HttpResponse response) {
+    return new CacheResult(isSuccessful(response), parseResponse(response));
+  }
+
+  private boolean isSuccessful(HttpResponse response) {
+    return response.getStatusLine().getStatusCode() == SC_NO_CONTENT;
+  }
+
+  private String parseResponse(HttpResponse response) {
+    HttpEntity entity = response.getEntity();
+    String asString = "";
+    if (entity != null) {
+      try {
+        asString = EntityUtils.toString(entity);
+      } catch (IOException e) {
+        log.error("Error parsing entity", e);
+      }
+    }
+    return asString;
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/evictcache/Configuration.java b/src/main/java/com/ericsson/gerrit/plugins/evictcache/Configuration.java
new file mode 100644
index 0000000..e342f88
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/evictcache/Configuration.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+class Configuration {
+  private static final int DEFAULT_TIMEOUT_MS = 5000;
+  private static final int DEFAULT_MAX_TRIES = 5;
+  private static final int DEFAULT_RETRY_INTERVAL = 1000;
+  private static final int DEFAULT_THREAD_POOL_SIZE = 1;
+
+  private final String url;
+  private final String user;
+  private final String password;
+  private final int connectionTimeout;
+  private final int socketTimeout;
+  private final int maxTries;
+  private final int retryInterval;
+  private final int threadPoolSize;
+
+  @Inject
+  Configuration(PluginConfigFactory config,
+      @PluginName String pluginName) {
+    PluginConfig cfg = config.getFromGerritConfig(pluginName, true);
+    url = Strings.nullToEmpty(cfg.getString("url"));
+    user = Strings.nullToEmpty(cfg.getString("user"));
+    password = Strings.nullToEmpty(cfg.getString("password"));
+    connectionTimeout = cfg.getInt("connectionTimeout", DEFAULT_TIMEOUT_MS);
+    socketTimeout = cfg.getInt("socketTimeout", DEFAULT_TIMEOUT_MS);
+    maxTries = cfg.getInt("maxTries", DEFAULT_MAX_TRIES);
+    retryInterval = cfg.getInt("retryInterval", DEFAULT_RETRY_INTERVAL);
+    threadPoolSize = cfg.getInt("threadPoolSize", DEFAULT_THREAD_POOL_SIZE);
+  }
+
+  int getConnectionTimeout() {
+    return connectionTimeout;
+  }
+
+  int getMaxTries() {
+    return maxTries;
+  }
+
+  int getRetryInterval() {
+    return retryInterval;
+  }
+
+  int getSocketTimeout() {
+    return socketTimeout;
+  }
+
+  String getUrl() {
+    return CharMatcher.is('/').trimTrailingFrom(url);
+  }
+
+  String getUser() {
+    return user;
+  }
+
+  String getPassword() {
+    return password;
+  }
+
+  int getThreadPoolSize() {
+    return threadPoolSize;
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/evictcache/Context.java b/src/main/java/com/ericsson/gerrit/plugins/evictcache/Context.java
new file mode 100644
index 0000000..0b0519b
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/evictcache/Context.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+/**
+ * Allows to tag a forwarded event to avoid infinitely looping events.
+ */
+class Context {
+  private static final ThreadLocal<Boolean> FORWARDED_EVENT =
+      new ThreadLocal<Boolean>() {
+        @Override
+        protected Boolean initialValue() {
+          return false;
+        }
+      };
+
+  private Context() {
+  }
+
+  static Boolean isForwardedEvent() {
+    return FORWARDED_EVENT.get();
+  }
+
+  static void setForwardedEvent() {
+    FORWARDED_EVENT.set(true);
+  }
+
+  static void unsetForwardedEvent() {
+    FORWARDED_EVENT.remove();
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheExecutor.java b/src/main/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheExecutor.java
new file mode 100644
index 0000000..8106f55
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheExecutor.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+@interface EvictCacheExecutor {
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheExecutorProvider.java b/src/main/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheExecutorProvider.java
new file mode 100644
index 0000000..e1de403
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheExecutorProvider.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+
+@Singleton
+class EvictCacheExecutorProvider
+    implements Provider<ScheduledThreadPoolExecutor>, LifecycleListener {
+  private WorkQueue.Executor executor;
+
+  @Inject
+  EvictCacheExecutorProvider(WorkQueue workQueue,
+      @PluginName String pluginName,
+      Configuration config) {
+    executor = workQueue.createQueue(config.getThreadPoolSize(),
+        "Evict cache [" + pluginName + " plugin]");
+  }
+
+  @Override
+  public void start() {
+    // do nothing
+  }
+
+  @Override
+  public void stop() {
+    executor.shutdown();
+    executor.unregisterWorkQueue();
+    executor = null;
+  }
+
+  @Override
+  public ScheduledThreadPoolExecutor get() {
+    return executor;
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheHandler.java b/src/main/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheHandler.java
new file mode 100644
index 0000000..ecd2a9b
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheHandler.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import com.google.common.cache.RemovalNotification;
+import com.google.gerrit.server.cache.CacheRemovalListener;
+import com.google.inject.Inject;
+
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.regex.Pattern;
+
+class EvictCacheHandler<K, V> implements CacheRemovalListener<K, V> {
+  private final ScheduledThreadPoolExecutor executor;
+  private final RestSession restSession;
+  private final Pattern pattern;
+
+  @Inject
+  EvictCacheHandler(RestSession restSession,
+      @EvictCacheExecutor ScheduledThreadPoolExecutor executor) {
+    this.restSession = restSession;
+    this.executor = executor;
+    pattern = Pattern.compile(
+        "^accounts.*|^groups.*|ldap_groups|ldap_usernames|^project.*|sshkeys|web_sessions");
+  }
+
+  @Override
+  public void onRemoval(String pluginName, String cacheName,
+      RemovalNotification<K, V> notification) {
+    if (!Context.isForwardedEvent() && !notification.wasEvicted()
+        && isSynchronized(cacheName)) {
+      executor.execute(
+          new EvictCacheTask(pluginName, cacheName, notification.getKey()));
+    }
+  }
+
+  private boolean isSynchronized(String cacheName) {
+    return pattern.matcher(cacheName).matches();
+  }
+
+  class EvictCacheTask implements Runnable {
+    private String pluginName;
+    private String cacheName;
+    private Object key;
+
+    EvictCacheTask(String pluginName, String cacheName, Object key) {
+      this.pluginName = pluginName;
+      this.cacheName = cacheName;
+      this.key = key;
+    }
+
+    @Override
+    public void run() {
+      restSession.evict(pluginName, cacheName, key);
+    }
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheRestApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheRestApiServlet.java
new file mode 100644
index 0000000..cf30402
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheRestApiServlet.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
+
+import com.google.common.base.Splitter;
+import com.google.common.cache.Cache;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+class EvictCacheRestApiServlet extends HttpServlet {
+  private static final int CACHENAME_INDEX = 1;
+  private static final long serialVersionUID = -1L;
+  private static final String SOURCE_NAME = "gerrit";
+  private static final String PROJECT_LIST = "project_list";
+  private static final Logger logger =
+      LoggerFactory.getLogger(EvictCacheRestApiServlet.class);
+
+  private final DynamicMap<Cache<?, ?>> cacheMap;
+
+  @Inject
+  EvictCacheRestApiServlet(DynamicMap<Cache<?, ?>> cacheMap) {
+    this.cacheMap = cacheMap;
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest req, HttpServletResponse rsp)
+      throws IOException, ServletException {
+    rsp.setContentType("text/plain");
+    rsp.setCharacterEncoding("UTF-8");
+    try {
+      List<String> params = Splitter.on('/').splitToList(req.getPathInfo());
+      String cacheName = params.get(CACHENAME_INDEX);
+      String json = req.getReader().readLine();
+      Object key = GsonParser.fromJson(cacheName, json);
+      Cache<?, ?> cache = cacheMap.get(SOURCE_NAME, cacheName);
+      Context.setForwardedEvent();
+      evictCache(cache, cacheName, key);
+      rsp.setStatus(SC_NO_CONTENT);
+    } finally {
+      Context.unsetForwardedEvent();
+    }
+  }
+
+  private void evictCache(Cache<?, ?> cache, String cacheName, Object key) {
+    if (PROJECT_LIST.equals(cacheName)) {
+      // One key is holding the list of projects
+      cache.invalidateAll();
+    } else {
+      cache.invalidate(key);
+    }
+    logger.debug("Invalidated " + cacheName);
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/evictcache/GsonParser.java b/src/main/java/com/ericsson/gerrit/plugins/evictcache/GsonParser.java
new file mode 100644
index 0000000..d752c06
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/evictcache/GsonParser.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+final class GsonParser {
+  private static final String ACCOUNTS = "accounts";
+  private static final String GROUPS = "groups";
+  private static final String GROUPS_BYINCLUDE = "groups_byinclude";
+  private static final String GROUPS_MEMBERS = "groups_members";
+  private static final String PROJECT_LIST = "project_list";
+
+  private GsonParser() {
+  }
+
+  static Object fromJson(String cacheName, String json) {
+    Gson gson = new GsonBuilder().create();
+    Object key;
+    // Need to add a case for 'adv_bases'
+    switch (cacheName) {
+      case ACCOUNTS:
+        key = gson.fromJson(Strings.nullToEmpty(json).trim(), Account.Id.class);
+        break;
+      case GROUPS:
+        key = gson.fromJson(Strings.nullToEmpty(json).trim(),
+            AccountGroup.Id.class);
+        break;
+      case GROUPS_BYINCLUDE:
+      case GROUPS_MEMBERS:
+        key = gson.fromJson(Strings.nullToEmpty(json).trim(),
+            AccountGroup.UUID.class);
+        break;
+      case PROJECT_LIST:
+        key = gson.fromJson(Strings.nullToEmpty(json), Object.class);
+        break;
+      default:
+        key = gson.fromJson(Strings.nullToEmpty(json).trim(), String.class);
+    }
+    return key;
+  }
+
+  static String toJson(String cacheName, Object key) {
+    Gson gson = new GsonBuilder().create();
+    String json;
+    // Need to add a case for 'adv_bases'
+    switch (cacheName) {
+      case ACCOUNTS:
+        json = gson.toJson(key, Account.Id.class);
+        break;
+      case GROUPS:
+        json = gson.toJson(key, AccountGroup.Id.class);
+        break;
+      case GROUPS_BYINCLUDE:
+      case GROUPS_MEMBERS:
+        json = gson.toJson(key, AccountGroup.UUID.class);
+        break;
+      case PROJECT_LIST:
+      default:
+        json = gson.toJson(key);
+    }
+    return json;
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/evictcache/HttpClientProvider.java b/src/main/java/com/ericsson/gerrit/plugins/evictcache/HttpClientProvider.java
new file mode 100644
index 0000000..1653223
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/evictcache/HttpClientProvider.java
@@ -0,0 +1,205 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.HttpRequestRetryHandler;
+import org.apache.http.client.ServiceUnavailableRetryStrategy;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.config.Registry;
+import org.apache.http.config.RegistryBuilder;
+import org.apache.http.conn.HttpClientConnectionManager;
+import org.apache.http.conn.socket.ConnectionSocketFactory;
+import org.apache.http.conn.socket.PlainConnectionSocketFactory;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.protocol.HttpContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.URI;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * Provides an HTTP client with SSL capabilities.
+ */
+@Singleton
+class HttpClientProvider implements Provider<CloseableHttpClient> {
+  private static final Logger log =
+      LoggerFactory.getLogger(HttpClientProvider.class);
+  private static final int CONNECTIONS_PER_ROUTE = 100;
+  // Up to 2 target instances with the max number of connections per host:
+  private static final int MAX_CONNECTIONS = 2 * CONNECTIONS_PER_ROUTE;
+
+  private static final int ERROR_CODES = 500;
+
+  private static final int MAX_CONNECTION_INACTIVITY = 10000;
+
+  private final Configuration cfg;
+  private final SSLConnectionSocketFactory sslSocketFactory;
+
+  @Inject
+  HttpClientProvider(Configuration cfg) {
+    this.cfg = cfg;
+    this.sslSocketFactory = buildSslSocketFactory();
+  }
+
+  @Override
+  public CloseableHttpClient get() {
+    return HttpClients.custom().setSSLSocketFactory(sslSocketFactory)
+        .setConnectionManager(customConnectionManager())
+        .setDefaultCredentialsProvider(buildCredentials())
+        .setDefaultRequestConfig(customRequestConfig())
+        .setRetryHandler(customRetryHandler())
+        .setServiceUnavailableRetryStrategy(customServiceUnavailRetryStrategy())
+        .build();
+  }
+
+  private RequestConfig customRequestConfig() {
+    return RequestConfig.custom().setConnectTimeout(cfg.getConnectionTimeout())
+        .setSocketTimeout(cfg.getSocketTimeout())
+        .setConnectionRequestTimeout(cfg.getConnectionTimeout())
+        .build();
+  }
+
+  private HttpRequestRetryHandler customRetryHandler() {
+    return new HttpRequestRetryHandler() {
+
+      @Override
+      public boolean retryRequest(IOException exception, int executionCount,
+          HttpContext context) {
+        if (executionCount > cfg.getMaxTries()
+            || exception instanceof SSLException) {
+          return false;
+        }
+        logRetry(exception.getMessage());
+        try {
+          Thread.sleep(cfg.getRetryInterval());
+        } catch (InterruptedException e) {
+          log.debug("Ignoring InterruptedException", e);
+        }
+        return true;
+      }
+    };
+  }
+
+  private ServiceUnavailableRetryStrategy customServiceUnavailRetryStrategy() {
+    return new ServiceUnavailableRetryStrategy() {
+      @Override
+      public boolean retryRequest(HttpResponse response, int executionCount,
+          HttpContext context) {
+        if (executionCount > cfg.getMaxTries()) {
+          return false;
+        }
+        if (response.getStatusLine().getStatusCode() >= ERROR_CODES) {
+          logRetry(response.getStatusLine().getReasonPhrase());
+          return true;
+        }
+        return false;
+      }
+
+      @Override
+      public long getRetryInterval() {
+        return cfg.getRetryInterval();
+      }
+    };
+  }
+
+  private void logRetry(String cause) {
+    log.warn("Retrying request to '" + cfg.getUrl() + "' Cause: " + cause);
+  }
+
+  private HttpClientConnectionManager customConnectionManager() {
+    Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder
+        .<ConnectionSocketFactory> create().register("https", sslSocketFactory)
+        .register("http", PlainConnectionSocketFactory.INSTANCE).build();
+    PoolingHttpClientConnectionManager connManager =
+        new PoolingHttpClientConnectionManager(socketFactoryRegistry);
+    connManager.setDefaultMaxPerRoute(CONNECTIONS_PER_ROUTE);
+    connManager.setMaxTotal(MAX_CONNECTIONS);
+    connManager.setValidateAfterInactivity(MAX_CONNECTION_INACTIVITY);
+    return connManager;
+  }
+
+  private SSLConnectionSocketFactory buildSslSocketFactory() {
+    return new SSLConnectionSocketFactory(buildSslContext(),
+        new DummyHostnameVerifier());
+  }
+
+  private SSLContext buildSslContext() {
+    try {
+      TrustManager[] trustAllCerts =
+          new TrustManager[] {new DummyX509TrustManager()};
+      SSLContext context = SSLContext.getInstance("TLS");
+      context.init(null, trustAllCerts, null);
+      return context;
+    } catch (KeyManagementException | NoSuchAlgorithmException e) {
+      log.warn("Error building SSLContext object", e);
+      return null;
+    }
+  }
+
+  private BasicCredentialsProvider buildCredentials() {
+    URI uri = URI.create(cfg.getUrl());
+    BasicCredentialsProvider creds = new BasicCredentialsProvider();
+    creds.setCredentials(new AuthScope(uri.getHost(), uri.getPort()),
+        new UsernamePasswordCredentials(cfg.getUser(), cfg.getPassword()));
+    return creds;
+  }
+
+  private static class DummyX509TrustManager implements X509TrustManager {
+    @Override
+    public X509Certificate[] getAcceptedIssuers() {
+      return new X509Certificate[0];
+    }
+
+    @Override
+    public void checkClientTrusted(X509Certificate[] certs, String authType) {
+      // no check
+    }
+
+    @Override
+    public void checkServerTrusted(X509Certificate[] certs, String authType) {
+      // no check
+    }
+  }
+
+  private static class DummyHostnameVerifier implements HostnameVerifier {
+    @Override
+    public boolean verify(String hostname, SSLSession session) {
+      // always accept
+      return true;
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ericsson/gerrit/plugins/evictcache/HttpModule.java b/src/main/java/com/ericsson/gerrit/plugins/evictcache/HttpModule.java
new file mode 100644
index 0000000..27cf205
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/evictcache/HttpModule.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import com.google.gerrit.httpd.plugins.HttpPluginModule;
+
+class HttpModule extends HttpPluginModule {
+  @Override
+  protected void configureServlets() {
+    serve("/gerrit/evict/*").with(EvictCacheRestApiServlet.class);
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/evictcache/HttpSession.java b/src/main/java/com/ericsson/gerrit/plugins/evictcache/HttpSession.java
new file mode 100644
index 0000000..35fbd65
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/evictcache/HttpSession.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import com.google.common.net.MediaType;
+import com.google.inject.Inject;
+
+import com.ericsson.gerrit.plugins.evictcache.CacheResponseHandler.CacheResult;
+
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+import java.io.IOException;
+
+class HttpSession {
+  private final CloseableHttpClient httpClient;
+  private final String url;
+
+  @Inject
+  HttpSession(CloseableHttpClient httpClient,
+      @SyncUrl String url) {
+    this.httpClient = httpClient;
+    this.url = url;
+  }
+
+  CacheResult post(String endpoint, String json) throws IOException {
+    HttpPost post = new HttpPost(url + endpoint);
+    StringEntity params = new StringEntity(json.trim());
+    post.addHeader("Content-Type", MediaType.JSON_UTF_8.toString());
+    post.addHeader("Accept", MediaType.JSON_UTF_8.toString());
+    post.setEntity(params);
+    return httpClient.execute(post, new CacheResponseHandler());
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/evictcache/Module.java b/src/main/java/com/ericsson/gerrit/plugins/evictcache/Module.java
new file mode 100644
index 0000000..28ba756
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/evictcache/Module.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.cache.CacheRemovalListener;
+import com.google.inject.Provides;
+import com.google.inject.Scopes;
+
+import org.apache.http.impl.client.CloseableHttpClient;
+
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+
+class Module extends LifecycleModule {
+
+  @Override
+  protected void configure() {
+    bind(CloseableHttpClient.class).toProvider(HttpClientProvider.class)
+        .in(Scopes.SINGLETON);
+    bind(Configuration.class);
+    bind(HttpSession.class);
+    bind(RestSession.class);
+    bind(ScheduledThreadPoolExecutor.class)
+        .annotatedWith(EvictCacheExecutor.class)
+        .toProvider(EvictCacheExecutorProvider.class);
+    listener().to(EvictCacheExecutorProvider.class);
+    DynamicSet.bind(binder(), CacheRemovalListener.class).to(
+        EvictCacheHandler.class);
+  }
+
+  @Provides
+  @SyncUrl
+  String syncUrl(Configuration config) {
+    return config.getUrl();
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/evictcache/RestSession.java b/src/main/java/com/ericsson/gerrit/plugins/evictcache/RestSession.java
new file mode 100644
index 0000000..1295ce2
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/evictcache/RestSession.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import com.google.common.base.Joiner;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+class RestSession {
+  private static final Logger log = LoggerFactory.getLogger(RestSession.class);
+  private static final String EVICT = "evict";
+  private final HttpSession httpSession;
+  private final String pluginName;
+
+  @Inject
+  RestSession(HttpSession httpClient,
+      @PluginName String pluginName) {
+    this.httpSession = httpClient;
+    this.pluginName = pluginName;
+  }
+
+  boolean evict(String sourceName, String cacheName, Object key) {
+    try {
+      String json = GsonParser.toJson(cacheName, key);
+      String buildEndpoint = buildEndpoint(pluginName, sourceName, cacheName);
+      return httpSession
+          .post(buildEndpoint, json)
+          .isSuccessful();
+    } catch (IOException e) {
+      log.error("Error trying to evict for cache " + cacheName, e);
+      return false;
+    }
+  }
+
+  private String buildEndpoint(String pluginName, String sourceName,
+      String cacheName) {
+    return Joiner.on("/").join("/plugins", pluginName, sourceName, EVICT, cacheName);
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/evictcache/SyncUrl.java b/src/main/java/com/ericsson/gerrit/plugins/evictcache/SyncUrl.java
new file mode 100644
index 0000000..14c2880
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/evictcache/SyncUrl.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+@interface SyncUrl {
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
new file mode 100644
index 0000000..6236e8f
--- /dev/null
+++ b/src/main/resources/Documentation/about.md
@@ -0,0 +1,10 @@
+The @PLUGIN@ plugin allows to synchronize the eviction of caches between two
+Gerrit instances sharing the same repositories and database.
+
+The plugin needs to be installed in both instances and every time a cache
+eviction occurs in one of the instances, the other instance's cache is updated.
+This way, both caches are kept synchronized.
+
+For this synchronization to work, http must be enabled in both instances and the
+plugin must be configured with valid credentials. For further information, refer
+to [config](config.html) documentation.
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
new file mode 100644
index 0000000..b5b5ed7
--- /dev/null
+++ b/src/main/resources/Documentation/build.md
@@ -0,0 +1,106 @@
+Build
+=====
+
+This plugin is built with Buck.
+
+Two build modes are supported: Standalone and in Gerrit tree. Standalone
+build mode is recommended, as this mode doesn't require local Gerrit
+tree to exist.
+
+Build standalone
+----------------
+
+Clone bucklets library:
+
+```
+  git clone https://gerrit.googlesource.com/bucklets
+
+```
+and link it to @PLUGIN@ directory:
+
+```
+  cd @PLUGIN@ && ln -s ../bucklets .
+```
+
+Add link to the .buckversion file:
+
+```
+  cd @PLUGIN@ && ln -s bucklets/buckversion .buckversion
+```
+
+Add link to the .watchmanconfig file:
+
+```
+  cd @PLUGIN@ && ln -s bucklets/watchmanconfig .watchmanconfig
+```
+
+To build the plugin, issue the following command:
+
+```
+  buck build plugin
+```
+
+The output is created in:
+
+```
+  buck-out/gen/@PLUGIN@.jar
+```
+
+This project can be imported into the Eclipse IDE:
+
+```
+  ./bucklets/tools/eclipse.py
+```
+
+To execute the tests run:
+
+```
+  buck test
+```
+
+To build plugin sources run:
+
+```
+  buck build src
+```
+
+The output is created in:
+
+```
+  buck-out/gen/@PLUGIN@-sources.jar
+```
+
+Build in Gerrit tree
+--------------------
+
+Clone or link this plugin to the plugins directory of Gerrit's source
+tree, and issue the command:
+
+```
+  buck build plugins/@PLUGIN@
+```
+
+The output is created in:
+
+```
+  buck-out/gen/plugins/@PLUGIN@/@PLUGIN@.jar
+```
+
+This project can be imported into the Eclipse IDE:
+
+```
+  ./tools/eclipse/project.py
+```
+
+To execute the tests run:
+
+```
+  buck test --include @PLUGIN@
+```
+
+How to build the Gerrit Plugin API is described in the [Gerrit
+documentation](../../../Documentation/dev-buck.html#_extension_and_plugin_api_jar_files).
+
+[Back to @PLUGIN@ documentation index][index]
+
+[index]: index.html
\ No newline at end of file
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
new file mode 100644
index 0000000..da544a7
--- /dev/null
+++ b/src/main/resources/Documentation/config.md
@@ -0,0 +1,46 @@
+@PLUGIN@ Configuration
+=========================
+
+In order for the synchronization to work, the @PLUGIN@ plugin must be installed
+in both instances and the following fields should be specified in the
+corresponding Gerrit configuration file:
+
+File 'gerrit.config'
+--------------------
+
+plugin.@PLUGIN@.url
+:   Specify the URL for the secondary (target) instance.
+
+plugin.@PLUGIN@.user
+:   Username to connect to the secondary (target) instance.
+
+plugin.@PLUGIN@.password
+:   Password to connect to the secondary (target) instance. This value can
+    also be defined in secure.config.
+
+@PLUGIN@ plugin uses REST API calls to synchronize cache evictions in the target
+instance. It is possible to customize the parameters of the underlying http client
+doing these calls by specifying the following fields:
+
+@PLUGIN@.connectionTimeout
+:   Maximum interval of time in milliseconds the plugin waits for a connection
+    to the target instance. When not specified, the default value is set to 5000ms.
+
+@PLUGIN@.socketTimeout
+:   Maximum interval of time in milliseconds the plugin waits for a response from the
+    target instance once the connection has been established. When not specified,
+    the default value is set to 5000ms.
+
+@PLUGIN@.maxTries
+:   Maximum number of times the plugin should attempt to evict the cache in the
+    target instance. Setting this value to 0 will disable retries. When not
+    specified, the default value is 5. After this number of failed tries, an error
+    is logged so that admins can flush the cache manually.
+
+@PLUGIN@.retryInterval
+:   The interval of time in milliseconds between the subsequent auto-retries.
+    When not specified, the default value is set to 1000ms.
+
+@PLUGIN@.threadPoolSize
+:   Maximum number of threads used to send cache evictions to the target instance.
+    Defaults to 1.
diff --git a/src/test/java/com/ericsson/gerrit/plugins/evictcache/CacheResponseHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/evictcache/CacheResponseHandlerTest.java
new file mode 100644
index 0000000..1d2dd3e
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/evictcache/CacheResponseHandlerTest.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.expect;
+
+import com.ericsson.gerrit.plugins.evictcache.CacheResponseHandler.CacheResult;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.StatusLine;
+import org.apache.http.entity.StringEntity;
+import org.easymock.EasyMockSupport;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.UnsupportedEncodingException;
+
+public class CacheResponseHandlerTest extends EasyMockSupport {
+  private static final int ERROR = 400;
+  private static final int OK = 204;
+  private static final String EMPTY_ENTITY = "";
+  private static final String ERROR_ENTITY = "Error";
+
+  private CacheResponseHandler handler;
+
+  @Before
+  public void setUp() throws Exception {
+    handler = new CacheResponseHandler();
+  }
+
+  @Test
+  public void testIsSuccessful() throws Exception {
+    HttpResponse response = setupMocks(OK, EMPTY_ENTITY);
+    CacheResult result = handler.handleResponse(response);
+    assertThat(result.isSuccessful()).isTrue();
+    assertThat(result.getMessage()).isEmpty();
+  }
+
+  @Test
+  public void testIsNotSuccessful() throws Exception {
+    HttpResponse response = setupMocks(ERROR, ERROR_ENTITY);
+    CacheResult result = handler.handleResponse(response);
+    assertThat(result.isSuccessful()).isFalse();
+    assertThat(result.getMessage()).contains(ERROR_ENTITY);
+  }
+
+  private HttpResponse setupMocks(int httpCode, String entity)
+      throws UnsupportedEncodingException {
+    StatusLine status = createNiceMock(StatusLine.class);
+    expect(status.getStatusCode()).andReturn(httpCode).anyTimes();
+    HttpResponse response = createNiceMock(HttpResponse.class);
+    expect(response.getStatusLine()).andReturn(status).anyTimes();
+    expect(response.getEntity()).andReturn(new StringEntity(entity)).anyTimes();
+    replayAll();
+    return response;
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/evictcache/ConfigurationTest.java b/src/test/java/com/ericsson/gerrit/plugins/evictcache/ConfigurationTest.java
new file mode 100644
index 0000000..c884291
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/evictcache/ConfigurationTest.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.expect;
+
+import org.easymock.EasyMockSupport;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+
+public class ConfigurationTest extends EasyMockSupport {
+  private static final String PASS = "fakePass";
+  private static final String USER = "fakeUser";
+  private static final String URL = "fakeUrl";
+  private static final String EMPTY = "";
+  private static final int TIMEOUT = 5000;
+  private static final int MAX_TRIES = 5;
+  private static final int RETRY_INTERVAL = 1000;
+  private static final int THREAD_POOL_SIZE = 1;
+
+  private PluginConfigFactory cfgFactoryMock;
+  private PluginConfig configMock;
+  private Configuration configuration;
+  private String pluginName = "evict-cache";
+
+  @Before
+  public void setUp() throws Exception {
+    configMock = createNiceMock(PluginConfig.class);
+    cfgFactoryMock = createMock(PluginConfigFactory.class);
+    expect(cfgFactoryMock.getFromGerritConfig(pluginName, true))
+        .andStubReturn(configMock);
+  }
+
+  @Test
+  public void testValuesPresentInGerritConfig() throws Exception {
+    buildMocks(true);
+    assertThat(configuration.getUrl()).isEqualTo(URL);
+    assertThat(configuration.getUser()).isEqualTo(USER);
+    assertThat(configuration.getPassword()).isEqualTo(PASS);
+    assertThat(configuration.getConnectionTimeout()).isEqualTo(TIMEOUT);
+    assertThat(configuration.getSocketTimeout()).isEqualTo(TIMEOUT);
+    assertThat(configuration.getMaxTries()).isEqualTo(MAX_TRIES);
+    assertThat(configuration.getRetryInterval()).isEqualTo(RETRY_INTERVAL);
+    assertThat(configuration.getThreadPoolSize()).isEqualTo(THREAD_POOL_SIZE);
+  }
+
+  @Test
+  public void testValuesNotPresentInGerritConfig() throws Exception {
+    buildMocks(false);
+    assertThat(configuration.getUrl()).isEqualTo(EMPTY);
+    assertThat(configuration.getUser()).isEqualTo(EMPTY);
+    assertThat(configuration.getPassword()).isEqualTo(EMPTY);
+    assertThat(configuration.getConnectionTimeout()).isEqualTo(0);
+    assertThat(configuration.getSocketTimeout()).isEqualTo(0);
+    assertThat(configuration.getMaxTries()).isEqualTo(0);
+    assertThat(configuration.getRetryInterval()).isEqualTo(0);
+    assertThat(configuration.getThreadPoolSize()).isEqualTo(0);
+  }
+
+  @Test
+  public void testUrlTrailingSlashIsDropped() throws Exception {
+    expect(configMock.getString("url")).andReturn(URL + "/");
+    replayAll();
+    configuration = new Configuration(cfgFactoryMock, pluginName);
+    assertThat(configuration).isNotNull();
+    assertThat(configuration.getUrl()).isEqualTo(URL);
+  }
+
+  private void buildMocks(boolean values) {
+    expect(configMock.getString("url")).andReturn(values ? URL : null);
+    expect(configMock.getString("user")).andReturn(values ? USER : null);
+    expect(configMock.getString("password")).andReturn(values ? PASS : null);
+    expect(configMock.getInt("connectionTimeout", TIMEOUT))
+        .andReturn(values ? TIMEOUT : 0);
+    expect(configMock.getInt("socketTimeout", TIMEOUT))
+        .andReturn(values ? TIMEOUT : 0);
+    expect(configMock.getInt("maxTries", MAX_TRIES))
+        .andReturn(values ? MAX_TRIES : 0);
+    expect(configMock.getInt("retryInterval", RETRY_INTERVAL))
+        .andReturn(values ? RETRY_INTERVAL : 0);
+    expect(configMock.getInt("threadPoolSize", THREAD_POOL_SIZE))
+        .andReturn(values ? THREAD_POOL_SIZE : 0);
+    replayAll();
+    configuration = new Configuration(cfgFactoryMock, pluginName);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/evictcache/Constants.java b/src/test/java/com/ericsson/gerrit/plugins/evictcache/Constants.java
new file mode 100644
index 0000000..f7ba16c
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/evictcache/Constants.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+final class Constants {
+
+  private Constants() {
+  }
+
+  static final String URL = "http://localhost:18888";
+  static final String ENDPOINT_BASE = "/plugins/evict-cache/gerrit/evict/";
+  static final String PROJECT_LIST = "project_list";
+  static final String ACCOUNTS = "accounts";
+  static final String GROUPS = "groups";
+  static final String GROUPS_BYINCLUDE = "groups_byinclude";
+  static final String GROUPS_MEMBERS = "groups_members";
+  static final String DEFAULT = "projects";
+
+  static final int PORT = 18888;
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/evictcache/ContextTest.java b/src/test/java/com/ericsson/gerrit/plugins/evictcache/ContextTest.java
new file mode 100644
index 0000000..06c54ed
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/evictcache/ContextTest.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.easymock.EasyMockSupport;
+import org.junit.Test;
+
+public class ContextTest extends EasyMockSupport {
+
+  @Test
+  public void testInitialValueNotNull() throws Exception {
+    assertThat(Context.isForwardedEvent()).isNotNull();
+    assertThat(Context.isForwardedEvent()).isFalse();
+  }
+
+  @Test
+  public void testSetForwardedEvent() throws Exception {
+    Context.setForwardedEvent();
+    try {
+      assertThat(Context.isForwardedEvent()).isTrue();
+    } finally {
+      Context.unsetForwardedEvent();
+    }
+  }
+
+  @Test
+  public void testUnsetForwardedEvent() throws Exception {
+    Context.setForwardedEvent();
+    Context.unsetForwardedEvent();
+    assertThat(Context.isForwardedEvent()).isFalse();
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheExecutorProviderTest.java b/src/test/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheExecutorProviderTest.java
new file mode 100644
index 0000000..90193bb
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheExecutorProviderTest.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.git.WorkQueue;
+
+import org.easymock.EasyMockSupport;
+import org.junit.Before;
+import org.junit.Test;
+
+public class EvictCacheExecutorProviderTest extends EasyMockSupport {
+  private WorkQueue.Executor executorMock;
+  private EvictCacheExecutorProvider evictCacheExecutorProvider;
+
+  @PluginName String pluginName;
+
+  @Before
+  public void setUp() throws Exception {
+    executorMock = createStrictMock(WorkQueue.Executor.class);
+    WorkQueue workQueueMock = createNiceMock(WorkQueue.class);
+    expect(workQueueMock.createQueue(4,
+        "Evict cache [" + pluginName + " plugin]"))
+            .andReturn(executorMock);
+    Configuration configMock = createStrictMock(Configuration.class);
+    expect(configMock.getThreadPoolSize()).andReturn(4);
+    replayAll();
+    evictCacheExecutorProvider =
+        new EvictCacheExecutorProvider(workQueueMock, pluginName, configMock);
+  }
+
+  @Test
+  public void shouldReturnExecutor() throws Exception {
+    assertThat(evictCacheExecutorProvider.get()).isEqualTo(executorMock);
+  }
+
+  @Test
+  public void testStop() throws Exception {
+    resetAll();
+    executorMock.shutdown();
+    expectLastCall().once();
+    executorMock.unregisterWorkQueue();
+    expectLastCall().once();
+    replayAll();
+
+    evictCacheExecutorProvider.start();
+    assertThat(evictCacheExecutorProvider.get()).isEqualTo(executorMock);
+    evictCacheExecutorProvider.stop();
+    verifyAll();
+    assertThat(evictCacheExecutorProvider.get()).isNull();
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheHandlerTest.java
new file mode 100644
index 0000000..93002ab
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheHandlerTest.java
@@ -0,0 +1,140 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import static org.easymock.EasyMock.expect;
+
+import com.google.common.cache.RemovalNotification;
+
+import org.easymock.EasyMock;
+import org.easymock.EasyMockSupport;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.api.easymock.PowerMock;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(RemovalNotification.class)
+public class EvictCacheHandlerTest extends EasyMockSupport {
+  private static final String pluginName = "evict-cache";
+  private static final String cacheName = "project_list";
+  private static final String invalidCacheName = "INVALID";
+  private static final boolean MOCK_REST_CLIENT = true;
+  private static final boolean DO_NOT_MOCK_REST_CLIENT = false;
+  private static final boolean WAS_EVICTED = true;
+  private static final boolean WAS_NOT_EVICTED = false;
+
+  private EvictCacheHandler<Object, Object> evictCacheHandler;
+  private RemovalNotification<Object, Object> notification;
+  private ScheduledThreadPoolExecutor pool;
+  private RestSession restClient;
+
+  @Test
+  public void testEvictCacheHandler() {
+    setUpMocks(MOCK_REST_CLIENT, WAS_NOT_EVICTED);
+    EasyMock.replay(restClient);
+    evictCacheHandler.onRemoval(pluginName, cacheName, notification);
+    verifyAll();
+  }
+
+  @Test
+  public void testInvalidCacheName() {
+    setUpMocks(DO_NOT_MOCK_REST_CLIENT, WAS_NOT_EVICTED);
+    replayAll();
+    evictCacheHandler.onRemoval(pluginName, invalidCacheName, notification);
+    verifyAll();
+  }
+
+  @Test
+  public void testInvalidRemovalCause() {
+    setUpMocks(DO_NOT_MOCK_REST_CLIENT, WAS_EVICTED);
+    evictCacheHandler.onRemoval(pluginName, cacheName, notification);
+    verifyAll();
+  }
+
+  @Test
+  public void testInvalidRemovalCauseAndCacheName() {
+    setUpMocks(DO_NOT_MOCK_REST_CLIENT, WAS_EVICTED);
+    evictCacheHandler.onRemoval(pluginName, invalidCacheName, notification);
+    verifyAll();
+  }
+
+  @Test
+  public void testForwardedInvalidRemovalCauseAndCacheName() {
+    setUpMocks(DO_NOT_MOCK_REST_CLIENT, WAS_EVICTED);
+    Context.setForwardedEvent();
+    try {
+      evictCacheHandler.onRemoval(pluginName, invalidCacheName, notification);
+    } finally {
+      Context.unsetForwardedEvent();
+    }
+    verifyAll();
+  }
+
+  @Test
+  public void testEvictCacheHandlerIsForwarded() {
+    setUpMocks(DO_NOT_MOCK_REST_CLIENT, WAS_NOT_EVICTED);
+    Context.setForwardedEvent();
+    try {
+      evictCacheHandler.onRemoval(pluginName, cacheName, notification);
+    } finally {
+      Context.unsetForwardedEvent();
+    }
+    verifyAll();
+  }
+
+  @Test
+  public void testEvictCacheIsForwardedAndAlreadyEvicted() {
+    setUpMocks(DO_NOT_MOCK_REST_CLIENT, WAS_EVICTED);
+    Context.setForwardedEvent();
+    try {
+      evictCacheHandler.onRemoval(pluginName, cacheName, notification);
+    } finally {
+      Context.unsetForwardedEvent();
+    }
+    verifyAll();
+  }
+
+  @SuppressWarnings({"rawtypes", "unchecked"})
+  private void setUpMocks(boolean mockRestClient, boolean wasEvicted) {
+    notification = PowerMock.createMock(RemovalNotification.class);
+    pool = new PoolMock(1);
+    Object key = new Object();
+    if (mockRestClient) {
+      restClient = createMock(RestSession.class);
+      expect(restClient.evict(pluginName, cacheName, key)).andReturn(true);
+    } else {
+      restClient = null;
+    }
+    expect(notification.wasEvicted()).andReturn(wasEvicted);
+    expect(notification.getKey()).andReturn(key);
+    EasyMock.replay(notification);
+    evictCacheHandler = new EvictCacheHandler(restClient, pool);
+  }
+
+  private class PoolMock extends ScheduledThreadPoolExecutor {
+    PoolMock(int corePoolSize) {
+      super(corePoolSize);
+    }
+
+    @Override
+    public void execute(Runnable command) {
+      command.run();
+    }
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheIT.java b/src/test/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheIT.java
new file mode 100644
index 0000000..db900c5
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheIT.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.givenThat;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PluginDaemonTest;
+import com.google.gerrit.server.config.SitePaths;
+
+import com.github.tomakehurst.wiremock.http.Request;
+import com.github.tomakehurst.wiremock.http.RequestListener;
+import com.github.tomakehurst.wiremock.http.Response;
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+
+import org.apache.http.HttpStatus;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.Description;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
+
+@NoHttpd
+public class EvictCacheIT extends PluginDaemonTest {
+
+  private static final String PLUGIN_NAME = "evict-cache";
+
+  @Rule
+  public WireMockRule wireMockRule = new WireMockRule(Constants.PORT);
+
+  @Override
+  protected void beforeTest(Description description)
+      throws Exception {
+    setConfig("url", Constants.URL);
+    setConfig("user", "admin");
+    super.beforeTest(description);
+  }
+
+  @Test
+  public void flushAndSendPost() throws Exception {
+    final String flushRequest = Constants.ENDPOINT_BASE + Constants.PROJECT_LIST;
+    wireMockRule.addMockServiceRequestListener(new RequestListener() {
+      @Override
+      public void requestReceived(Request request, Response response) {
+        if (request.getAbsoluteUrl().contains(flushRequest)) {
+          synchronized (flushRequest) {
+            flushRequest.notify();
+          }
+        }
+      }
+    });
+    givenThat(post(urlEqualTo(flushRequest))
+        .willReturn(aResponse().withStatus(HttpStatus.SC_OK)));
+
+    adminSshSession.exec("gerrit flush-caches --cache " + Constants.PROJECT_LIST);
+    synchronized (flushRequest) {
+      flushRequest.wait(TimeUnit.SECONDS.toMillis(2));
+    }
+    verify(postRequestedFor(urlEqualTo(flushRequest)));
+  }
+
+  private void setConfig(String name, String value) throws Exception {
+    SitePaths sitePath = new SitePaths(tempSiteDir.getRoot().toPath());
+    FileBasedConfig cfg = getGerritConfigFile(sitePath);
+    cfg.load();
+    cfg.setString("plugin", PLUGIN_NAME, name, value);
+    cfg.save();
+  }
+
+  private FileBasedConfig getGerritConfigFile(SitePaths sitePath)
+      throws IOException {
+    FileBasedConfig cfg =
+        new FileBasedConfig(sitePath.gerrit_config.toFile(), FS.DETECTED);
+    if (!cfg.getFile().exists()) {
+      Path etc_path = Files.createDirectories(sitePath.etc_dir);
+      Files.createFile(etc_path.resolve("gerrit.config"));
+    }
+    return cfg;
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheRestApiServletTest.java b/src/test/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheRestApiServletTest.java
new file mode 100644
index 0000000..aea1586
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/evictcache/EvictCacheRestApiServletTest.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import static org.easymock.EasyMock.expect;
+
+import com.google.common.cache.Cache;
+import com.google.gerrit.extensions.registration.DynamicMap;
+
+import org.easymock.EasyMockSupport;
+import org.junit.Test;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class EvictCacheRestApiServletTest extends EasyMockSupport {
+  private static final String PLUGIN_NAME = "gerrit";
+  private HttpServletRequest request;
+  private HttpServletResponse response;
+  private BufferedReader reader;
+  private EvictCacheRestApiServlet servlet;
+  private DynamicMap<Cache<?, ?>> cacheMap;
+  @SuppressWarnings("rawtypes")
+  private Cache cache;
+
+  @Test
+  public void evictAccounts() throws IOException, ServletException {
+    setUp(Constants.ACCOUNTS);
+    servlet.doPost(request, response);
+    verifyAll();
+  }
+
+  @Test
+  public void evictProjectList() throws IOException, ServletException {
+    setUp(Constants.PROJECT_LIST);
+    servlet.doPost(request, response);
+    verifyAll();
+  }
+
+  @Test
+  public void evictGroups() throws IOException, ServletException {
+    setUp(Constants.GROUPS);
+    servlet.doPost(request, response);
+    verifyAll();
+  }
+
+  @Test
+  public void evictGroupsByInclude() throws IOException, ServletException {
+    setUp(Constants.GROUPS_BYINCLUDE);
+    servlet.doPost(request, response);
+    verifyAll();
+  }
+
+  @Test
+  public void evictGroupsMembers() throws IOException, ServletException {
+    setUp(Constants.GROUPS_MEMBERS);
+    servlet.doPost(request, response);
+    verifyAll();
+  }
+
+  @Test
+  public void evictDefault() throws IOException, ServletException {
+    setUp(Constants.DEFAULT);
+    servlet.doPost(request, response);
+    verifyAll();
+  }
+
+  @SuppressWarnings("unchecked")
+  private void setUp(String cacheName) throws IOException {
+    resetAll();
+    cacheMap = createMock(DynamicMap.class);
+    request = createMock(HttpServletRequest.class);
+    reader = createMock(BufferedReader.class);
+    response = createNiceMock(HttpServletResponse.class);
+    cache = createNiceMock(Cache.class);
+
+    expect(cacheMap.get(PLUGIN_NAME, cacheName)).andReturn(cache);
+    servlet = new EvictCacheRestApiServlet(cacheMap);
+    expect(request.getPathInfo()).andReturn("/" + cacheName);
+    expect(request.getReader()).andReturn(reader);
+
+    if (Constants.DEFAULT.equals(cacheName)) {
+      expect(reader.readLine()).andReturn("abc");
+    } else if (Constants.GROUPS_BYINCLUDE.equals(cacheName)
+        || Constants.GROUPS_MEMBERS.equals(cacheName)) {
+      expect(reader.readLine()).andReturn("{\"uuid\":\"abcd1234\"}");
+    } else {
+      expect(reader.readLine()).andReturn("{}");
+    }
+    replayAll();
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/evictcache/GsonParserTest.java b/src/test/java/com/ericsson/gerrit/plugins/evictcache/GsonParserTest.java
new file mode 100644
index 0000000..638456d
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/evictcache/GsonParserTest.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+import org.junit.Test;
+
+public class GsonParserTest {
+  private static final Object EMPTY_JSON = "{}";
+
+  @Test
+  public void AccountIDParse() {
+    Account.Id accountId = new Account.Id(1);
+    String json = GsonParser.toJson(Constants.ACCOUNTS, accountId);
+    assertThat(accountId)
+        .isEqualTo(GsonParser.fromJson(Constants.ACCOUNTS, json));
+  }
+
+  @Test
+  public void AccountGroupIDParse() {
+    AccountGroup.Id accountGroupId = new AccountGroup.Id(1);
+    String json = GsonParser.toJson(Constants.GROUPS, accountGroupId);
+    assertThat(accountGroupId)
+        .isEqualTo(GsonParser.fromJson(Constants.GROUPS, json));
+  }
+
+  @Test
+  public void AccountGroupUUIDParse() {
+    AccountGroup.UUID accountGroupUuid = new AccountGroup.UUID("abc123");
+    String json =
+        GsonParser.toJson(Constants.GROUPS_BYINCLUDE, accountGroupUuid);
+    assertThat(accountGroupUuid)
+        .isEqualTo(GsonParser.fromJson(Constants.GROUPS_BYINCLUDE, json));
+  }
+
+  @Test
+  public void StringParse() {
+    String key = "key";
+    String json = GsonParser.toJson(Constants.DEFAULT, key);
+    assertThat(key).isEqualTo(GsonParser.fromJson(Constants.DEFAULT, json));
+  }
+
+  @Test
+  public void NoKeyParse() {
+    Object object = new Object();
+    String json = GsonParser.toJson(Constants.PROJECT_LIST, object);
+    assertThat(json).isEqualTo(EMPTY_JSON);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/evictcache/HttpClientProviderTest.java b/src/test/java/com/ericsson/gerrit/plugins/evictcache/HttpClientProviderTest.java
new file mode 100644
index 0000000..eb6cbf5
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/evictcache/HttpClientProviderTest.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.expect;
+
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Scopes;
+
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.easymock.EasyMockSupport;
+import org.junit.Before;
+import org.junit.Test;
+
+public class HttpClientProviderTest extends EasyMockSupport {
+  private static final int TIME_INTERVAL = 1000;
+  private static final String EMPTY = "";
+
+  private Configuration config;
+
+  @Before
+  public void setUp() throws Exception {
+    config = createNiceMock(Configuration.class);
+    expect(config.getUrl()).andReturn(EMPTY).anyTimes();
+    expect(config.getUser()).andReturn(EMPTY).anyTimes();
+    expect(config.getPassword()).andReturn(EMPTY).anyTimes();
+    expect(config.getMaxTries()).andReturn(1).anyTimes();
+    expect(config.getConnectionTimeout()).andReturn(TIME_INTERVAL).anyTimes();
+    expect(config.getSocketTimeout()).andReturn(TIME_INTERVAL).anyTimes();
+    expect(config.getRetryInterval()).andReturn(TIME_INTERVAL).anyTimes();
+    replayAll();
+  }
+
+  @Test
+  public void testGet() throws Exception {
+    Injector injector = Guice.createInjector(new TestModule());
+    CloseableHttpClient httpClient1 =
+        injector.getInstance(CloseableHttpClient.class);
+    assertThat(httpClient1).isNotNull();
+    CloseableHttpClient httpClient2 =
+        injector.getInstance(CloseableHttpClient.class);
+    assertThat(httpClient1).isEqualTo(httpClient2);
+  }
+
+  class TestModule extends LifecycleModule {
+    @Override
+    protected void configure() {
+      bind(Configuration.class).toInstance(config);
+      bind(CloseableHttpClient.class).toProvider(HttpClientProvider.class)
+          .in(Scopes.SINGLETON);
+    }
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/evictcache/HttpSessionTest.java b/src/test/java/com/ericsson/gerrit/plugins/evictcache/HttpSessionTest.java
new file mode 100644
index 0000000..d11e648
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/evictcache/HttpSessionTest.java
@@ -0,0 +1,191 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.expect;
+
+import com.ericsson.gerrit.plugins.evictcache.CacheResponseHandler.CacheResult;
+import com.github.tomakehurst.wiremock.http.Fault;
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import com.github.tomakehurst.wiremock.stubbing.Scenario;
+
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.easymock.EasyMockSupport;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.net.SocketTimeoutException;
+
+public class HttpSessionTest extends EasyMockSupport {
+  private static final int MAX_TRIES = 3;
+  private static final int RETRY_INTERVAL = 250;
+  private static final int TIMEOUT = 500;
+  private static final int ERROR = 500;
+  private static final int OK = 204;
+  private static final int NOT_FOUND = 404;
+  private static final int UNAUTHORIZED = 401;
+
+  private static final String ENDPOINT = Constants.ENDPOINT_BASE + "cache";
+  private static final String EMPTYJSON = "{}";
+  private static final String ERROR_MESSAGE = "Error message";
+  private static final String REQUEST_MADE = "Request made";
+  private static final String SECOND_TRY = "Second try";
+  private static final String THIRD_TRY = "Third try";
+  private static final String RETRY_AT_ERROR = "Retry at error";
+  private static final String RETRY_AT_DELAY = "Retry at delay";
+
+  private Configuration cfg;
+  private CloseableHttpClient httpClient;
+  private HttpSession httpSession;
+
+  @Rule
+  public WireMockRule wireMockRule = new WireMockRule(Constants.PORT);
+
+  @Before
+  public void setUp() throws Exception {
+    cfg = createMock(Configuration.class);
+    expect(cfg.getUrl()).andReturn(Constants.URL).anyTimes();
+    expect(cfg.getUser()).andReturn("user");
+    expect(cfg.getPassword()).andReturn("pass");
+    expect(cfg.getMaxTries()).andReturn(MAX_TRIES).anyTimes();
+    expect(cfg.getConnectionTimeout()).andReturn(TIMEOUT).anyTimes();
+    expect(cfg.getSocketTimeout()).andReturn(TIMEOUT).anyTimes();
+    expect(cfg.getRetryInterval()).andReturn(RETRY_INTERVAL).anyTimes();
+    replayAll();
+    httpClient = new HttpClientProvider(cfg).get();
+    httpSession = new HttpSession(httpClient, Constants.URL);
+  }
+
+  @Test
+  public void testResponseOK() throws Exception {
+    stubFor(post(urlEqualTo(ENDPOINT)).willReturn(aResponse().withStatus(OK)));
+    assertThat(httpSession.post(ENDPOINT, EMPTYJSON).isSuccessful()).isTrue();
+  }
+
+  @Test
+  public void testNotAuthorized() throws Exception {
+    String expected = "unauthorized";
+    stubFor(post(urlEqualTo(ENDPOINT)).willReturn(
+        aResponse().withStatus(UNAUTHORIZED).withBody(expected)));
+
+    CacheResult result = httpSession.post(ENDPOINT, EMPTYJSON);
+    assertThat(result.isSuccessful()).isFalse();
+    assertThat(result.getMessage()).isEqualTo(expected);
+  }
+
+  @Test
+  public void testNotFound() throws Exception {
+    String expected = "not found";
+    stubFor(post(urlEqualTo(ENDPOINT)).willReturn(
+        aResponse().withStatus(NOT_FOUND).withBody(expected)));
+
+    CacheResult result = httpSession.post(ENDPOINT, EMPTYJSON);
+    assertThat(result.isSuccessful()).isFalse();
+    assertThat(result.getMessage()).isEqualTo(expected);
+  }
+
+  @Test
+  public void testBadResponseRetryThenOK() throws Exception {
+    stubFor(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_ERROR)
+        .whenScenarioStateIs(Scenario.STARTED).willSetStateTo(REQUEST_MADE)
+        .willReturn(aResponse().withStatus(ERROR)));
+    stubFor(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_ERROR)
+        .whenScenarioStateIs(REQUEST_MADE)
+        .willReturn(aResponse().withStatus(OK)));
+
+    assertThat(httpSession.post(ENDPOINT, EMPTYJSON).isSuccessful()).isTrue();
+  }
+
+  @Test
+  public void testBadResponseRetryThenGiveUp() throws Exception {
+    stubFor(post(urlEqualTo(ENDPOINT)).willReturn(
+        aResponse().withStatus(ERROR).withBody(ERROR_MESSAGE)));
+
+    assertThat(httpSession.post(ENDPOINT, EMPTYJSON).isSuccessful()).isFalse();
+    assertThat(httpSession.post(ENDPOINT, EMPTYJSON).getMessage())
+        .isEqualTo(ERROR_MESSAGE);
+  }
+
+  @Test
+  public void testRetryAfterDelay() throws Exception {
+    stubFor(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_DELAY)
+        .whenScenarioStateIs(Scenario.STARTED).willSetStateTo(REQUEST_MADE)
+        .willReturn(aResponse().withStatus(ERROR).withFixedDelay(TIMEOUT / 2)));
+    stubFor(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_DELAY)
+        .whenScenarioStateIs(REQUEST_MADE)
+        .willReturn(aResponse().withStatus(OK)));
+
+    assertThat(httpSession.post(ENDPOINT, EMPTYJSON).isSuccessful()).isTrue();
+  }
+
+  @Test
+  public void testRetryAfterTimeoutThenOK() throws Exception {
+    wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_DELAY)
+        .whenScenarioStateIs(Scenario.STARTED).willSetStateTo(REQUEST_MADE)
+        .willReturn(aResponse().withFixedDelay(TIMEOUT)));
+    wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_DELAY)
+        .whenScenarioStateIs(REQUEST_MADE).willSetStateTo(SECOND_TRY)
+        .willReturn(aResponse().withFixedDelay(TIMEOUT)));
+    wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_DELAY)
+        .whenScenarioStateIs(SECOND_TRY).willSetStateTo(THIRD_TRY)
+        .willReturn(aResponse().withFixedDelay(TIMEOUT)));
+    wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_DELAY)
+        .whenScenarioStateIs(THIRD_TRY)
+        .willReturn(aResponse().withStatus(OK)));
+
+    assertThat(httpSession.post(ENDPOINT, EMPTYJSON).isSuccessful()).isTrue();
+  }
+
+  @Test(expected = SocketTimeoutException.class)
+  public void testMaxRetriesAfterTimeoutThenGiveUp() throws Exception {
+    wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_DELAY)
+        .whenScenarioStateIs(Scenario.STARTED).willSetStateTo(REQUEST_MADE)
+        .willReturn(aResponse().withFixedDelay(TIMEOUT)));
+    wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_DELAY)
+        .whenScenarioStateIs(REQUEST_MADE).willSetStateTo(SECOND_TRY)
+        .willReturn(aResponse().withFixedDelay(TIMEOUT)));
+    wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_DELAY)
+        .whenScenarioStateIs(SECOND_TRY).willSetStateTo(THIRD_TRY)
+        .willReturn(aResponse().withFixedDelay(TIMEOUT)));
+    wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_DELAY)
+        .whenScenarioStateIs(THIRD_TRY)
+        .willReturn(aResponse().withFixedDelay(TIMEOUT)));
+
+    httpSession.post(ENDPOINT, EMPTYJSON);
+  }
+
+  @Test
+  public void testGiveUpAtTimeout() throws Exception {
+    stubFor(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_DELAY)
+        .whenScenarioStateIs(Scenario.STARTED).willSetStateTo(REQUEST_MADE)
+        .willReturn(aResponse().withStatus(ERROR).withFixedDelay(TIMEOUT)));
+
+    assertThat(httpSession.post(ENDPOINT, EMPTYJSON).isSuccessful()).isFalse();
+  }
+
+  @Test
+  public void testResponseWithMalformedResponse() throws Exception {
+    stubFor(post(urlEqualTo(ENDPOINT)).willReturn(
+        aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK)));
+
+    assertThat(httpSession.post(ENDPOINT, EMPTYJSON).isSuccessful()).isFalse();
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/evictcache/ModuleTest.java b/src/test/java/com/ericsson/gerrit/plugins/evictcache/ModuleTest.java
new file mode 100644
index 0000000..3262101
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/evictcache/ModuleTest.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.expect;
+
+import org.easymock.EasyMockSupport;
+import org.junit.Test;
+
+public class ModuleTest extends EasyMockSupport {
+
+  @Test
+  public void testSyncUrlProvider() {
+    Configuration configMock = createNiceMock(Configuration.class);
+    String expected = "someUrl";
+    expect(configMock.getUrl()).andReturn(expected);
+    replayAll();
+    Module module = new Module();
+    assertThat(module.syncUrl(configMock)).isEqualTo(expected);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/evictcache/RestSessionTest.java b/src/test/java/com/ericsson/gerrit/plugins/evictcache/RestSessionTest.java
new file mode 100644
index 0000000..0801dfa
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/evictcache/RestSessionTest.java
@@ -0,0 +1,112 @@
+// Copyright (C) 2015 Ericsson
+//
+// 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.ericsson.gerrit.plugins.evictcache;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.expect;
+
+import com.google.common.base.Joiner;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+import com.ericsson.gerrit.plugins.evictcache.CacheResponseHandler.CacheResult;
+
+import org.easymock.EasyMockSupport;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class RestSessionTest extends EasyMockSupport {
+  private static final String EVICT = "evict";
+  private static final String SOURCE_NAME = "gerrit";
+  private static final String PLUGIN_NAME = "evict-cache";
+  private static final String EMPTY_JSON = "{}";
+  private static final String EMPTY_JSON2 = "\"{}\"";
+  private static final String ID_RESPONSE = "{\"id\":0}";
+
+  private RestSession restClient;
+
+  @Test
+  public void testEvictCacheOK() throws Exception {
+    setupMocks(Constants.DEFAULT, EMPTY_JSON2, true, false);
+    assertThat(restClient.evict(SOURCE_NAME, Constants.DEFAULT, EMPTY_JSON))
+        .isTrue();
+  }
+
+  @Test
+  public void testEvictAccountsOK() throws Exception {
+    setupMocks(Constants.ACCOUNTS, ID_RESPONSE, true, false);
+    assertThat(restClient.evict(SOURCE_NAME, Constants.ACCOUNTS,
+        createMock(Account.Id.class))).isTrue();
+  }
+
+  @Test
+  public void testEvictGroupsOK() throws Exception {
+    setupMocks(Constants.GROUPS, ID_RESPONSE, true, false);
+    assertThat(restClient.evict(SOURCE_NAME, Constants.GROUPS,
+        createMock(AccountGroup.Id.class))).isTrue();
+  }
+
+  @Test
+  public void testEvictGroupsByIncludeOK() throws Exception {
+    setupMocks(Constants.GROUPS_BYINCLUDE, EMPTY_JSON, true, false);
+    assertThat(restClient.evict(SOURCE_NAME, Constants.GROUPS_BYINCLUDE,
+        createMock(AccountGroup.UUID.class))).isTrue();
+  }
+
+  @Test
+  public void testEvictGroupsMembersOK() throws Exception {
+    setupMocks(Constants.GROUPS_MEMBERS, EMPTY_JSON, true, false);
+    assertThat(restClient.evict(SOURCE_NAME, Constants.GROUPS_MEMBERS,
+        createMock(AccountGroup.UUID.class))).isTrue();
+  }
+
+  @Test
+  public void testEvictProjectListOK() throws Exception {
+    setupMocks(Constants.PROJECT_LIST, EMPTY_JSON, true, false);
+    assertThat(
+        restClient.evict(SOURCE_NAME, Constants.PROJECT_LIST, new Object()))
+            .isTrue();
+  }
+
+  @Test
+  public void testEvictCacheFailed() throws Exception {
+    setupMocks(Constants.DEFAULT, EMPTY_JSON2, false, false);
+    assertThat(restClient.evict(SOURCE_NAME, Constants.DEFAULT, EMPTY_JSON))
+        .isFalse();
+  }
+
+  @Test
+  public void testEvictCacheThrowsException() throws Exception {
+    setupMocks(Constants.DEFAULT, EMPTY_JSON2, false, true);
+    assertThat(restClient.evict(SOURCE_NAME, Constants.DEFAULT, EMPTY_JSON))
+        .isFalse();
+  }
+
+  private void setupMocks(String cacheName, String json, boolean ok,
+      boolean exception) throws IOException {
+    String request = Joiner.on("/").join("/plugins", PLUGIN_NAME, SOURCE_NAME,
+        EVICT, cacheName);
+    HttpSession httpSession = createNiceMock(HttpSession.class);
+    if (exception) {
+      expect(httpSession.post(request, json)).andThrow(new IOException());
+    } else {
+      CacheResult result = new CacheResult(ok, "Error");
+      expect(httpSession.post(request, json)).andReturn(result);
+    }
+    replayAll();
+    restClient = new RestSession(httpSession, PLUGIN_NAME);
+  }
+}