Initialize sync-index plugin with first version

Sync-index plugin keeps the secondary index synchronized among two
Gerrit instances that share the same git repositories and Gerrit
database.

Using a new extension point in Gerrit core [1], this plugin is notified
every time a change has been indexed in or removed from the secondary
index. Then it propagates that information to the configured target
Gerrit instance. The sync-index plugin installed in the target instance
updates the corresponding secondary index accordingly.

[1] https://gerrit-review.googlesource.com/#/c/72607/

Change-Id: Id5c089cfe3263f23dd56880e47d609180aeb3edc
diff --git a/.buckconfig b/.buckconfig
new file mode 100644
index 0000000..2b818b6
--- /dev/null
+++ b/.buckconfig
@@ -0,0 +1,16 @@
+[alias]
+  sync-index = //:sync-index
+  plugin = //:sync-index
+  src = //:sync-index-sources
+
+[java]
+  jar_spool_mode = direct_to_jar
+  src_roots = java, resources
+
+[project]
+  ignore = .git, eclipse-out/
+  parallel_parsing = true
+
+[cache]
+  mode = dir
+  dir = buck-out/cache
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7f7fd4b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+/.buckversion
+/.classpath
+/.project
+/.settings/
+/.watchmanconfig
+/buck-out/
+/bucklets
+/eclipse-out/
diff --git a/BUCK b/BUCK
new file mode 100644
index 0000000..163c42f
--- /dev/null
+++ b/BUCK
@@ -0,0 +1,53 @@
+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/**/*'])
+
+TEST_DEPS = GERRIT_PLUGIN_API + GERRIT_TESTS + [
+  ':sync-index__plugin',
+  ':wiremock',
+]
+
+gerrit_plugin(
+  name = 'sync-index',
+  srcs = SOURCES,
+  resources = RESOURCES,
+  manifest_entries = [
+    'Gerrit-PluginName: sync-index',
+    'Gerrit-ApiType: plugin',
+    'Gerrit-Module: com.ericsson.gerrit.plugins.syncindex.Module',
+    'Gerrit-HttpModule: com.ericsson.gerrit.plugins.syncindex.HttpModule',
+    'Implementation-Title: sync-index plugin',
+    'Implementation-URL: https://gerrit.ericsson.se/#/admin/projects/gerrit/plugins/sync-index',
+    'Implementation-Vendor: Ericsson',
+  ],
+  provided_deps = GERRIT_TESTS + [':wiremock',],
+)
+
+java_sources(
+  name = 'sync-index-sources',
+  srcs = SOURCES + RESOURCES,
+)
+
+java_library(
+  name = 'classpath',
+  deps = TEST_DEPS,
+)
+
+java_test(
+  name = 'sync-index_tests',
+  srcs = glob(['src/test/java/**/*.java']),
+  labels = ['sync-index'],
+  source_under_test = [':sync-index__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/gerrit/BUCK b/lib/gerrit/BUCK
new file mode 100644
index 0000000..f30d88f
--- /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 = 'acceptance-framework',
+  id = 'com.google.gerrit:gerrit-acceptance-framework:' + VER,
+  license = 'Apache2.0',
+  attach_source = False,
+  repository = REPO,
+)
+
+maven_jar(
+  name = 'plugin-api',
+  id = 'com.google.gerrit:gerrit-plugin-api:' + VER,
+  license = 'Apache2.0',
+  attach_source = False,
+  repository = REPO,
+)
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/Configuration.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/Configuration.java
new file mode 100644
index 0000000..a93ba84
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/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.syncindex;
+
+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;
+  }
+
+  public int getThreadPoolSize() {
+    return threadPoolSize;
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/Context.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/Context.java
new file mode 100644
index 0000000..67abc1e
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/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.syncindex;
+
+/**
+ * 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(Boolean b) {
+    FORWARDED_EVENT.set(b);
+  }
+
+  static void unsetForwardedEvent() {
+    FORWARDED_EVENT.remove();
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpClientProvider.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpClientProvider.java
new file mode 100644
index 0000000..c36370c
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpClientProvider.java
@@ -0,0 +1,193 @@
+// 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.syncindex;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+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.NoopHostnameVerifier;
+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.SSLContext;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * Provides an HTTP client with SSL capabilities.
+ */
+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.debug("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(),
+        NoopHostnameVerifier.INSTANCE);
+  }
+
+  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
+    }
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpModule.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpModule.java
new file mode 100644
index 0000000..334c195
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/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.syncindex;
+
+import com.google.gerrit.httpd.plugins.HttpPluginModule;
+
+class HttpModule extends HttpPluginModule {
+  @Override
+  protected void configureServlets() {
+    serveRegex("/index/\\d+$").with(SyncIndexRestApiServlet.class);
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpSession.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpSession.java
new file mode 100644
index 0000000..159acad
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/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.syncindex;
+
+import com.google.inject.Inject;
+
+import com.ericsson.gerrit.plugins.syncindex.IndexResponseHandler.IndexResult;
+
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpPost;
+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;
+  }
+
+  IndexResult post(String endpoint) throws IOException {
+    return httpClient.execute(new HttpPost(url + endpoint),
+        new IndexResponseHandler());
+  }
+
+  IndexResult delete(String endpoint) throws IOException {
+    return httpClient.execute(new HttpDelete(url + endpoint),
+        new IndexResponseHandler());
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/IndexEventHandler.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/IndexEventHandler.java
new file mode 100644
index 0000000..f8e2f31
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/IndexEventHandler.java
@@ -0,0 +1,103 @@
+// 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.syncindex;
+
+import com.google.common.base.Objects;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.extensions.events.ChangeIndexedListener;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+
+class IndexEventHandler implements ChangeIndexedListener {
+  private final Executor executor;
+  private final RestSession restClient;
+  private final String pluginName;
+  private final Set<SyncIndexTask> queuedTasks = Collections
+      .newSetFromMap(new ConcurrentHashMap<SyncIndexTask, Boolean>());
+
+  @Inject
+  IndexEventHandler(@SyncIndexExecutor Executor executor,
+      @PluginName String pluginName,
+      RestSession restClient) {
+    this.restClient = restClient;
+    this.executor = executor;
+    this.pluginName = pluginName;
+  }
+
+  @Override
+  public void onChangeIndexed(ChangeData cd) {
+    executeIndexTask(cd.getId(), false);
+  }
+
+  @Override
+  public void onChangeDeleted(Change.Id id) {
+    executeIndexTask(id, true);
+  }
+
+  private void executeIndexTask(Change.Id id, boolean deleted) {
+    if (!Context.isForwardedEvent()) {
+      SyncIndexTask syncIndexTask = new SyncIndexTask(id.get(), deleted);
+      if (queuedTasks.add(syncIndexTask)) {
+        executor.execute(syncIndexTask);
+      }
+    }
+  }
+
+  class SyncIndexTask implements Runnable {
+    private int changeId;
+    private boolean deleted;
+
+    SyncIndexTask(int changeId, boolean deleted) {
+      this.changeId = changeId;
+      this.deleted = deleted;
+    }
+
+    @Override
+    public void run() {
+      queuedTasks.remove(this);
+      if (deleted) {
+        restClient.deleteFromIndex(changeId);
+      } else {
+        restClient.index(changeId);
+      }
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(changeId, deleted);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (!(obj instanceof SyncIndexTask)) {
+        return false;
+      }
+      SyncIndexTask other = (SyncIndexTask) obj;
+      return changeId == other.changeId && deleted == other.deleted;
+    }
+
+    @Override
+    public String toString() {
+      return String.format("[%s] Index change %s in target instance",
+          pluginName, changeId);
+    }
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/IndexResponseHandler.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/IndexResponseHandler.java
new file mode 100644
index 0000000..2d549ae
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/IndexResponseHandler.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.syncindex;
+
+import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
+
+import com.ericsson.gerrit.plugins.syncindex.IndexResponseHandler.IndexResult;
+
+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 IndexResponseHandler implements ResponseHandler<IndexResult> {
+
+  static class IndexResult {
+    private boolean successful;
+    private String message;
+
+    IndexResult(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(IndexResponseHandler.class);
+
+  @Override
+  public IndexResult handleResponse(HttpResponse response) {
+    return new IndexResult(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/syncindex/Module.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/Module.java
new file mode 100644
index 0000000..5b278c3
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/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.syncindex;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.extensions.events.ChangeIndexedListener;
+import com.google.inject.Provides;
+import com.google.inject.Scopes;
+
+import org.apache.http.impl.client.CloseableHttpClient;
+
+import java.util.concurrent.Executor;
+
+class Module extends LifecycleModule {
+
+  @Override
+  protected void configure() {
+    bind(CloseableHttpClient.class).toProvider(HttpClientProvider.class)
+        .in(Scopes.SINGLETON);
+    bind(Configuration.class).in(Scopes.SINGLETON);
+    bind(HttpSession.class);
+    bind(RestSession.class);
+    bind(Executor.class)
+        .annotatedWith(SyncIndexExecutor.class)
+        .toProvider(SyncIndexExecutorProvider.class);
+    listener().to(SyncIndexExecutorProvider.class);
+    DynamicSet.bind(binder(), ChangeIndexedListener.class).to(
+        IndexEventHandler.class);
+  }
+
+  @Provides
+  @SyncUrl
+  String syncUrl(Configuration config) {
+    return config.getUrl();
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/RestSession.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/RestSession.java
new file mode 100644
index 0000000..9cdd4ea
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/RestSession.java
@@ -0,0 +1,71 @@
+// 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.syncindex;
+
+import com.google.common.base.Joiner;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.inject.Inject;
+
+import com.ericsson.gerrit.plugins.syncindex.IndexResponseHandler.IndexResult;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+class RestSession {
+  private static final Logger log = LoggerFactory.getLogger(RestSession.class);
+
+  private final HttpSession httpSession;
+  private final String pluginName;
+
+  @Inject
+  RestSession(HttpSession httpClient, @PluginName String pluginName) {
+    this.httpSession = httpClient;
+    this.pluginName = pluginName;
+  }
+
+  boolean index(int changeId) {
+    try {
+      IndexResult result = httpSession.post(buildEndpoint(changeId));
+      if (result.isSuccessful()) {
+        return true;
+      }
+      log.error("Unable to index change {}. Cause: {}", changeId,
+          result.getMessage());
+    } catch (IOException e) {
+      log.error("Error trying to index change " + changeId, e);
+    }
+    return false;
+  }
+
+  boolean deleteFromIndex(int changeId) {
+    try {
+      IndexResult result = httpSession.delete(buildEndpoint(changeId));
+      if (result.isSuccessful()) {
+        return true;
+      }
+      log.error("Unable to delete from index change {}. Cause: {}", changeId,
+          result.getMessage());
+    } catch (IOException e) {
+      log.error("Error trying to delete from index change " + changeId, e);
+    }
+    return false;
+  }
+
+  private String buildEndpoint(int changeId) {
+    return Joiner.on("/").join("/plugins", pluginName, "index", changeId);
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutor.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutor.java
new file mode 100644
index 0000000..75932e9
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutor.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.syncindex;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+@interface SyncIndexExecutor {
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutorProvider.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutorProvider.java
new file mode 100644
index 0000000..0400506
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutorProvider.java
@@ -0,0 +1,52 @@
+// 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.syncindex;
+
+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.Executor;
+
+@Singleton
+class SyncIndexExecutorProvider implements Provider<Executor>,
+    LifecycleListener {
+  private WorkQueue.Executor executor;
+
+  @Inject
+  SyncIndexExecutorProvider(WorkQueue workQueue, Configuration config) {
+    executor =
+        workQueue.createQueue(config.getThreadPoolSize(), "Sync remote index");
+  }
+
+  @Override
+  public void start() {
+    //do nothing
+  }
+
+  @Override
+  public void stop() {
+    executor.shutdown();
+    executor.unregisterWorkQueue();
+    executor = null;
+  }
+
+  @Override
+  public Executor get() {
+    return executor;
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexRestApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexRestApiServlet.java
new file mode 100644
index 0000000..ff3e47b
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexRestApiServlet.java
@@ -0,0 +1,105 @@
+// 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.syncindex;
+
+import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
+import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+class SyncIndexRestApiServlet extends HttpServlet {
+  private static final long serialVersionUID = -1L;
+  private static final Logger logger =
+      LoggerFactory.getLogger(SyncIndexRestApiServlet.class);
+
+  private final ChangeIndexer indexer;
+  private final SchemaFactory<ReviewDb> schemaFactory;
+
+  @Inject
+  SyncIndexRestApiServlet(ChangeIndexer indexer,
+      SchemaFactory<ReviewDb> schemaFactory) {
+    this.indexer = indexer;
+    this.schemaFactory = schemaFactory;
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest req, HttpServletResponse rsp)
+      throws IOException, ServletException {
+    rsp.setContentType("text/plain");
+    rsp.setCharacterEncoding("UTF-8");
+    Change.Id id = getIdFromRequest(req.getPathInfo());
+
+    try (ReviewDb db = schemaFactory.open()) {
+      Context.setForwardedEvent(true);
+      Change change = db.changes().get(id);
+      if (change == null) {
+        throw new NoSuchChangeException(id);
+      }
+      indexer.index(db, change);
+      rsp.setStatus(SC_NO_CONTENT);
+    } catch (IOException e) {
+      rsp.sendError(SC_CONFLICT, e.getMessage());
+      logger.error("Unable to update index", e);
+    } catch (OrmException | NoSuchChangeException e) {
+      rsp.sendError(SC_NOT_FOUND, "Change not found\n");
+      logger.debug("Error trying to find a change ", e);
+    } finally {
+      Context.unsetForwardedEvent();
+    }
+  }
+
+  @Override
+  protected void doDelete(HttpServletRequest req, HttpServletResponse rsp)
+      throws IOException, ServletException {
+    rsp.setContentType("text/plain");
+    rsp.setCharacterEncoding("UTF-8");
+    Change.Id id = getIdFromRequest(req.getPathInfo());
+
+    try {
+      Context.setForwardedEvent(true);
+      indexer.delete(id);
+      rsp.setStatus(SC_NO_CONTENT);
+    } catch (IOException e) {
+      rsp.sendError(SC_CONFLICT, e.getMessage());
+      logger.error("Unable to update index", e);
+    } finally {
+      Context.unsetForwardedEvent();
+    }
+  }
+
+  private Change.Id getIdFromRequest(String path) {
+    String changeId = path.substring(path.lastIndexOf('/') + 1);
+    return Change.Id.parse(changeId);
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncUrl.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncUrl.java
new file mode 100644
index 0000000..8b3ac1a
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/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.syncindex;
+
+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..a711d86
--- /dev/null
+++ b/src/main/resources/Documentation/about.md
@@ -0,0 +1,11 @@
+The @PLUGIN@ plugin allows to synchronize secondary indexes between two Gerrit
+instances sharing the same git repositories and database.
+
+The plugin is installed in both instances and every time the secondary index
+is modified in one of the instances, i.e., a change is added, updated or removed
+from the index, the other instance index is updated accordingly. This way, both
+indexes are kept synchronized.
+
+For this secondary index 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..3babed2
--- /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
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
new file mode 100644
index 0000000..48b9e94
--- /dev/null
+++ b/src/main/resources/Documentation/config.md
@@ -0,0 +1,51 @@
+@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 = target_instance_url
+:  user = username
+:  password = password
+
+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 index changes 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 index the event 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 re-index the change 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 index events to the target instance.
+    Defaults to 1.
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/ConfigurationTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/ConfigurationTest.java
new file mode 100644
index 0000000..fd39331
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/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.syncindex;
+
+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 = "sync-index";
+
+  @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/syncindex/ContextTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/ContextTest.java
new file mode 100644
index 0000000..f43439e
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/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.syncindex;
+
+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(true);
+    try {
+      assertThat(Context.isForwardedEvent()).isTrue();
+    } finally {
+      Context.unsetForwardedEvent();
+    }
+  }
+
+  @Test
+  public void testUnsetForwardedEvent() throws Exception {
+    Context.setForwardedEvent(true);
+    Context.unsetForwardedEvent();
+    assertThat(Context.isForwardedEvent()).isFalse();
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/HttpClientProviderTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/HttpClientProviderTest.java
new file mode 100644
index 0000000..48af47a
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/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.syncindex;
+
+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/syncindex/HttpSessionTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/HttpSessionTest.java
new file mode 100644
index 0000000..27f30d5
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/HttpSessionTest.java
@@ -0,0 +1,154 @@
+// 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.syncindex;
+
+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.urlEqualTo;
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.expect;
+
+import com.ericsson.gerrit.plugins.syncindex.IndexResponseHandler.IndexResult;
+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.ClassRule;
+import org.junit.Test;
+
+public class HttpSessionTest extends EasyMockSupport {
+  private static final int MAX_TRIES = 5;
+  private static final int RETRY_INTERVAL = 1000;
+  private static final int TIMEOUT = 1000;
+  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 = "/plugins/sync-index/index/1";
+  private static final String ERROR_MESSAGE = "Error message";
+  private static final String REQUEST_MADE = "Request made";
+  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;
+
+  @ClassRule
+  public static WireMockRule wireMockRule = new WireMockRule(0);
+
+  @Before
+  public void setUp() throws Exception {
+    String url = "http://localhost:" + wireMockRule.port();
+    cfg = createMock(Configuration.class);
+    expect(cfg.getUrl()).andReturn(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, url);
+
+    wireMockRule.resetRequests();
+  }
+
+  @Test
+  public void testResponseOK() throws Exception {
+    wireMockRule.givenThat(post(urlEqualTo(ENDPOINT))
+        .willReturn(aResponse().withStatus(OK)));
+
+    assertThat(httpSession.post(ENDPOINT).isSuccessful()).isTrue();
+  }
+
+  @Test
+  public void testNotAuthorized() throws Exception {
+    String expected = "unauthorized";
+    wireMockRule.givenThat(post(urlEqualTo(ENDPOINT))
+        .willReturn(aResponse().withStatus(UNAUTHORIZED).withBody(expected)));
+
+    IndexResult result = httpSession.post(ENDPOINT);
+    assertThat(result.isSuccessful()).isFalse();
+    assertThat(result.getMessage()).isEqualTo(expected);
+  }
+
+  @Test
+  public void testNotFound() throws Exception {
+    String expected = "not found";
+    wireMockRule.givenThat(post(urlEqualTo(ENDPOINT))
+        .willReturn(aResponse().withStatus(NOT_FOUND).withBody(expected)));
+
+    IndexResult result = httpSession.post(ENDPOINT);
+    assertThat(result.isSuccessful()).isFalse();
+    assertThat(result.getMessage()).isEqualTo(expected);
+  }
+
+  @Test
+  public void testBadResponseRetryThenOK() throws Exception {
+    wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_ERROR)
+        .whenScenarioStateIs(Scenario.STARTED).willSetStateTo(REQUEST_MADE)
+        .willReturn(aResponse().withStatus(ERROR)));
+    wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_ERROR)
+        .whenScenarioStateIs(REQUEST_MADE)
+        .willReturn(aResponse().withStatus(OK)));
+
+    assertThat(httpSession.post(ENDPOINT).isSuccessful()).isTrue();
+  }
+
+  @Test
+  public void testBadResponseRetryThenGiveUp() throws Exception {
+    wireMockRule.givenThat(post(urlEqualTo(ENDPOINT))
+        .willReturn(aResponse().withStatus(ERROR).withBody(ERROR_MESSAGE)));
+
+    IndexResult result = httpSession.post(ENDPOINT);
+    assertThat(result.isSuccessful()).isFalse();
+    assertThat(result.getMessage()).isEqualTo(ERROR_MESSAGE);
+  }
+
+  @Test
+  public void testRetryAfterDelay() throws Exception {
+    wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_DELAY)
+        .whenScenarioStateIs(Scenario.STARTED).willSetStateTo(REQUEST_MADE)
+        .willReturn(aResponse().withStatus(ERROR).withFixedDelay(TIMEOUT / 2)));
+    wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_DELAY)
+        .whenScenarioStateIs(REQUEST_MADE)
+        .willReturn(aResponse().withStatus(OK)));
+
+    assertThat(httpSession.post(ENDPOINT).isSuccessful()).isTrue();
+  }
+
+  @Test
+  public void testGiveUpAtTimeout() throws Exception {
+    wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_DELAY)
+        .whenScenarioStateIs(Scenario.STARTED).willSetStateTo(REQUEST_MADE)
+        .willReturn(aResponse().withStatus(ERROR).withFixedDelay(TIMEOUT)));
+
+    assertThat(httpSession.post(ENDPOINT).isSuccessful()).isFalse();
+  }
+
+  @Test
+  public void testResponseWithMalformedResponse() throws Exception {
+    wireMockRule.givenThat(post(urlEqualTo(ENDPOINT))
+        .willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK)));
+
+    assertThat(httpSession.post(ENDPOINT).isSuccessful()).isFalse();
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/IndexEventHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/IndexEventHandlerTest.java
new file mode 100644
index 0000000..614d30f
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/IndexEventHandlerTest.java
@@ -0,0 +1,141 @@
+// 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.syncindex;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.reset;
+
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.client.KeyUtil;
+import com.google.gwtorm.server.StandardKeyEncoder;
+
+import com.ericsson.gerrit.plugins.syncindex.IndexEventHandler.SyncIndexTask;
+
+import org.easymock.EasyMockSupport;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.util.concurrent.Executor;
+
+public class IndexEventHandlerTest extends EasyMockSupport {
+  private static final String PLUGIN_NAME = "sync-index";
+  private static final int CHANGE_ID = 1;
+
+  private IndexEventHandler indexEventHandler;
+  private Executor poolMock;
+  private RestSession restClientMock;
+  private ChangeData cd;
+  private Change.Id id;
+
+  @BeforeClass
+  public static void setUp() {
+    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
+  }
+
+  @Before
+  public void setUpMocks() {
+    cd = createNiceMock(ChangeData.class);
+    id = Change.Id.parse(Integer.toString(CHANGE_ID));
+    expect(cd.getId()).andReturn(id).anyTimes();
+    poolMock = createMock(Executor.class);
+    poolMock.execute(anyObject(SyncIndexTask.class));
+    expectLastCall().andDelegateTo(MoreExecutors.directExecutor());
+    restClientMock = createMock(RestSession.class);
+    indexEventHandler =
+        new IndexEventHandler(poolMock, PLUGIN_NAME, restClientMock);
+  }
+
+  @Test
+  public void shouldIndexInRemoteOnChangeIndexedEvent() throws Exception {
+    expect(restClientMock.index(CHANGE_ID)).andReturn(true);
+    replayAll();
+    indexEventHandler.onChangeIndexed(cd);
+    verifyAll();
+  }
+
+  @Test
+  public void shouldDeleteFromIndexInRemoteOnChangeDeletedEvent()
+      throws Exception {
+    reset(cd);
+    expect(restClientMock.deleteFromIndex(CHANGE_ID)).andReturn(true);
+    replayAll();
+    indexEventHandler.onChangeDeleted(id);
+    verifyAll();
+  }
+
+  @Test
+  public void shouldNotCallRemoteWhenEventIsForwarded() throws Exception {
+    reset(poolMock);
+    replayAll();
+    Context.setForwardedEvent(true);
+    indexEventHandler.onChangeIndexed(cd);
+    indexEventHandler.onChangeDeleted(id);
+    Context.unsetForwardedEvent();
+    verifyAll();
+  }
+
+  @Test
+  public void duplicateEventOfAQueuedEventShouldGetDiscarded() {
+    reset(poolMock);
+    poolMock.execute(indexEventHandler.new SyncIndexTask(CHANGE_ID, false));
+    expectLastCall().once();
+    replayAll();
+    indexEventHandler.onChangeIndexed(cd);
+    indexEventHandler.onChangeIndexed(cd);
+    verifyAll();
+  }
+
+  @Test
+  public void testSyncIndexTaskToString() throws Exception {
+    SyncIndexTask syncIndexTask =
+        indexEventHandler.new SyncIndexTask(CHANGE_ID, false);
+    assertThat(syncIndexTask.toString()).isEqualTo(
+        String.format("[%s] Index change %s in target instance", PLUGIN_NAME,
+            CHANGE_ID));
+  }
+
+  @Test
+  public void testSyncIndexTaskHashCodeAndEquals() {
+    SyncIndexTask task = indexEventHandler.new SyncIndexTask(CHANGE_ID, false);
+
+    assertThat(task.equals(task)).isTrue();
+    assertThat(task.hashCode()).isEqualTo(task.hashCode());
+
+    SyncIndexTask identicalTask =
+        indexEventHandler.new SyncIndexTask(CHANGE_ID, false);
+    assertThat(task.equals(identicalTask)).isTrue();
+    assertThat(task.hashCode()).isEqualTo(identicalTask.hashCode());
+
+    assertThat(task.equals(null)).isFalse();
+    assertThat(task.equals("test")).isFalse();
+    assertThat(task.hashCode()).isNotEqualTo("test".hashCode());
+
+    SyncIndexTask differentChangeIdTask =
+        indexEventHandler.new SyncIndexTask(123, false);
+    assertThat(task.equals(differentChangeIdTask)).isFalse();
+    assertThat(task.hashCode()).isNotEqualTo(differentChangeIdTask.hashCode());
+
+    SyncIndexTask removeTask =
+        indexEventHandler.new SyncIndexTask(CHANGE_ID, true);
+    assertThat(task.equals(removeTask)).isFalse();
+    assertThat(task.hashCode()).isNotEqualTo(removeTask.hashCode());
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/IndexResponseHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/IndexResponseHandlerTest.java
new file mode 100644
index 0000000..595cf7d
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/IndexResponseHandlerTest.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.syncindex;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.expect;
+
+import com.ericsson.gerrit.plugins.syncindex.IndexResponseHandler.IndexResult;
+
+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 IndexResponseHandlerTest 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 IndexResponseHandler handler;
+
+  @Before
+  public void setUp() throws Exception {
+    handler = new IndexResponseHandler();
+  }
+
+  @Test
+  public void testIsSuccessful() throws Exception {
+    HttpResponse response = setupMocks(OK, EMPTY_ENTITY);
+    IndexResult result = handler.handleResponse(response);
+    assertThat(result.isSuccessful()).isTrue();
+    assertThat(result.getMessage()).isEmpty();
+  }
+
+  @Test
+  public void testIsNotSuccessful() throws Exception {
+    HttpResponse response = setupMocks(ERROR, ERROR_ENTITY);
+    IndexResult 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/syncindex/ModuleTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/ModuleTest.java
new file mode 100644
index 0000000..bfc42b4
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/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.syncindex;
+
+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/syncindex/RestSessionTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/RestSessionTest.java
new file mode 100644
index 0000000..31850c2
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/RestSessionTest.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.syncindex;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.expect;
+
+import com.google.common.base.Joiner;
+
+import com.ericsson.gerrit.plugins.syncindex.IndexResponseHandler.IndexResult;
+
+import org.easymock.EasyMockSupport;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class RestSessionTest extends EasyMockSupport {
+  private static final int CHANGE_NUMBER = 1;
+  private static final String DELETE_OP = "delete";
+  private static final String INDEX_OP = "index";
+  private static final String PLUGIN_NAME = "sync-index";
+  private static final String EMPTY_MSG = "";
+  private static final String ERROR_MSG = "Error";
+  private static final String EXCEPTION_MSG = "Exception";
+  private static final boolean SUCCESSFUL = true;
+  private static final boolean FAILED = false;
+  private static final boolean DO_NOT_THROW_EXCEPTION = false;
+  private static final boolean THROW_EXCEPTION = true;
+
+  private RestSession restClient;
+
+  @Test
+  public void testIndexChangeOK() throws Exception {
+    setUpMocks(INDEX_OP, SUCCESSFUL, EMPTY_MSG, DO_NOT_THROW_EXCEPTION);
+    assertThat(restClient.index(CHANGE_NUMBER)).isTrue();
+  }
+
+  @Test
+  public void testIndexChangeFailed() throws Exception {
+    setUpMocks(INDEX_OP, FAILED, ERROR_MSG, DO_NOT_THROW_EXCEPTION);
+    assertThat(restClient.index(CHANGE_NUMBER)).isFalse();
+  }
+
+  @Test
+  public void testIndexChangeThrowsException() throws Exception {
+    setUpMocks(INDEX_OP, FAILED, EXCEPTION_MSG, THROW_EXCEPTION);
+    assertThat(restClient.index(CHANGE_NUMBER)).isFalse();
+  }
+
+  @Test
+  public void testChangeDeletedFromIndexOK() throws Exception {
+    setUpMocks(DELETE_OP, SUCCESSFUL, EMPTY_MSG, DO_NOT_THROW_EXCEPTION);
+    assertThat(restClient.deleteFromIndex(CHANGE_NUMBER)).isTrue();
+  }
+
+  @Test
+  public void testChangeDeletedFromIndexFailed() throws Exception {
+    setUpMocks(DELETE_OP, FAILED, ERROR_MSG, DO_NOT_THROW_EXCEPTION);
+    assertThat(restClient.deleteFromIndex(CHANGE_NUMBER)).isFalse();
+  }
+
+  @Test
+  public void testChangeDeletedFromThrowsException() throws Exception {
+    setUpMocks(DELETE_OP, FAILED, EXCEPTION_MSG, THROW_EXCEPTION);
+    assertThat(restClient.deleteFromIndex(CHANGE_NUMBER)).isFalse();
+  }
+
+  private void setUpMocks(String operation, boolean isOperationSuccessful,
+      String msg, boolean exception) throws Exception {
+    String request =
+        Joiner.on("/").join("/plugins", PLUGIN_NAME, INDEX_OP, CHANGE_NUMBER);
+    HttpSession httpSession = createNiceMock(HttpSession.class);
+    if (exception) {
+      if (operation.equals(INDEX_OP)) {
+        expect(httpSession.post(request)).andThrow(new IOException());
+      } else {
+        expect(httpSession.delete(request)).andThrow(new IOException());
+      }
+    } else {
+      IndexResult result = new IndexResult(isOperationSuccessful, msg);
+      if (operation.equals(INDEX_OP)) {
+        expect(httpSession.post(request)).andReturn(result);
+      } else {
+        expect(httpSession.delete(request)).andReturn(result);
+      }
+    }
+    restClient = new RestSession(httpSession, PLUGIN_NAME);
+    replayAll();
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutorProviderTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutorProviderTest.java
new file mode 100644
index 0000000..665b10a
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutorProviderTest.java
@@ -0,0 +1,64 @@
+// 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.syncindex;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+
+import com.google.gerrit.server.git.WorkQueue;
+
+import org.easymock.EasyMockSupport;
+import org.junit.Before;
+import org.junit.Test;
+
+public class SyncIndexExecutorProviderTest extends EasyMockSupport {
+  private WorkQueue.Executor executorMock;
+  private SyncIndexExecutorProvider syncIndexExecutorProvider;
+
+  @Before
+  public void setUp() throws Exception {
+    executorMock = createStrictMock(WorkQueue.Executor.class);
+    WorkQueue workQueueMock = createNiceMock(WorkQueue.class);
+    expect(workQueueMock.createQueue(4, "Sync remote index")).andReturn(
+        executorMock);
+    Configuration configMock = createStrictMock(Configuration.class);
+    expect(configMock.getThreadPoolSize()).andReturn(4);
+    replayAll();
+    syncIndexExecutorProvider =
+        new SyncIndexExecutorProvider(workQueueMock, configMock);
+  }
+
+  @Test
+  public void shouldReturnExecutor() throws Exception {
+    assertThat(syncIndexExecutorProvider.get()).isEqualTo(executorMock);
+  }
+
+  @Test
+  public void testStop() throws Exception {
+    resetAll();
+    executorMock.shutdown();
+    expectLastCall().once();
+    executorMock.unregisterWorkQueue();
+    expectLastCall().once();
+    replayAll();
+
+    syncIndexExecutorProvider.start();
+    assertThat(syncIndexExecutorProvider.get()).isEqualTo(executorMock);
+    syncIndexExecutorProvider.stop();
+    verifyAll();
+    assertThat(syncIndexExecutorProvider.get()).isNull();
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexRestApiServletTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexRestApiServletTest.java
new file mode 100644
index 0000000..2dbdd83
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexRestApiServletTest.java
@@ -0,0 +1,168 @@
+// Copyright (C) 2016 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.syncindex;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ChangeAccess;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gwtorm.client.KeyUtil;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.gwtorm.server.StandardKeyEncoder;
+
+import org.easymock.EasyMockSupport;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class SyncIndexRestApiServletTest extends EasyMockSupport {
+  private static final boolean CHANGE_EXISTS = true;
+  private static final boolean CHANGE_DOES_NOT_EXIST = false;
+  private static final boolean DO_NOT_THROW_IO_EXCEPTION = false;
+  private static final boolean DO_NOT_THROW_ORM_EXCEPTION = false;
+  private static final boolean THROW_IO_EXCEPTION = true;
+  private static final boolean THROW_ORM_EXCEPTION = true;
+  private static final String CHANGE_NUMBER = "1";
+
+  private ChangeIndexer indexer;
+  private SchemaFactory<ReviewDb> schemaFactory;
+  private SyncIndexRestApiServlet syncIndexRestApiServlet;
+  private HttpServletRequest req;
+  private HttpServletResponse rsp;
+  private Change.Id id;
+
+  @BeforeClass
+  public static void setup() {
+    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
+  }
+
+  @Before
+  @SuppressWarnings("unchecked")
+  public void setUpMocks() {
+    indexer = createNiceMock(ChangeIndexer.class);
+    schemaFactory = createNiceMock(SchemaFactory.class);
+    req = createNiceMock(HttpServletRequest.class);
+    rsp = createNiceMock(HttpServletResponse.class);
+    syncIndexRestApiServlet =
+        new SyncIndexRestApiServlet(indexer, schemaFactory);
+    id = Change.Id.parse(CHANGE_NUMBER);
+
+    expect(req.getPathInfo()).andReturn("/index/" + CHANGE_NUMBER);
+  }
+
+  @Test
+  public void changeIsIndexed() throws Exception {
+    setupPostMocks(CHANGE_EXISTS);
+    verifyPost();
+  }
+
+  @Test
+  public void changeToIndexDoNotExist() throws Exception {
+    setupPostMocks(CHANGE_DOES_NOT_EXIST);
+    verifyPost();
+  }
+
+  @Test
+  public void schemaThrowsExceptionWhenLookingUpForChange() throws Exception {
+    setupPostMocks(CHANGE_EXISTS, THROW_ORM_EXCEPTION);
+    verifyPost();
+  }
+
+  @Test
+  public void indexerThrowsIOExceptionTryingToIndexChange() throws Exception {
+    setupPostMocks(CHANGE_EXISTS, DO_NOT_THROW_ORM_EXCEPTION,
+        THROW_IO_EXCEPTION);
+    verifyPost();
+  }
+
+  @Test
+  public void changeIsDeletedFromIndex() throws Exception {
+    setupDeleteMocks(DO_NOT_THROW_IO_EXCEPTION);
+    verifyDelete();
+  }
+
+  @Test
+  public void indexerThrowsExceptionTryingToDeleteChange() throws Exception {
+    setupDeleteMocks(THROW_IO_EXCEPTION);
+    verifyDelete();
+  }
+
+  private void setupPostMocks(boolean changeExist) throws Exception {
+    setupPostMocks(changeExist, DO_NOT_THROW_ORM_EXCEPTION,
+        DO_NOT_THROW_IO_EXCEPTION);
+  }
+
+  private void setupPostMocks(boolean changeExist, boolean ormException)
+      throws OrmException, IOException {
+    setupPostMocks(changeExist, ormException, DO_NOT_THROW_IO_EXCEPTION);
+  }
+
+  private void setupPostMocks(boolean changeExist, boolean ormException,
+      boolean ioException) throws OrmException, IOException {
+    if (ormException) {
+      expect(schemaFactory.open()).andThrow(new OrmException(""));
+    } else {
+      ReviewDb db = createNiceMock(ReviewDb.class);
+      expect(schemaFactory.open()).andReturn(db);
+      ChangeAccess ca = createNiceMock(ChangeAccess.class);
+      expect(db.changes()).andReturn(ca);
+
+      if (changeExist) {
+        Change change = new Change(null, id, null, null, TimeUtil.nowTs());
+        expect(ca.get(id)).andReturn(change);
+        indexer.index(db, change);
+        if (ioException) {
+          expectLastCall().andThrow(new IOException());
+        } else {
+          expectLastCall().once();
+        }
+      } else {
+        expect(ca.get(id)).andReturn(null);
+      }
+    }
+    replayAll();
+  }
+
+  private void verifyPost() throws IOException, ServletException {
+    syncIndexRestApiServlet.doPost(req, rsp);
+    verifyAll();
+  }
+
+  private void setupDeleteMocks(boolean exception) throws IOException {
+    indexer.delete(id);
+    if (exception) {
+      expectLastCall().andThrow(new IOException());
+    } else {
+      expectLastCall().once();
+    }
+    replayAll();
+  }
+
+  private void verifyDelete() throws IOException, ServletException {
+    syncIndexRestApiServlet.doDelete(req, rsp);
+    verifyAll();
+  }
+}