You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zipkin.apache.org by ad...@apache.org on 2019/05/08 07:06:14 UTC

[incubator-zipkin] branch server-tests-kotlin created (now f7f4232)

This is an automated email from the ASF dual-hosted git repository.

adriancole pushed a change to branch server-tests-kotlin
in repository https://gitbox.apache.org/repos/asf/incubator-zipkin.git.


      at f7f4232  Ports all zipkin-server tests to Kotlin

This branch includes the following new commits:

     new f7f4232  Ports all zipkin-server tests to Kotlin

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[incubator-zipkin] 01/01: Ports all zipkin-server tests to Kotlin

Posted by ad...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

adriancole pushed a commit to branch server-tests-kotlin
in repository https://gitbox.apache.org/repos/asf/incubator-zipkin.git

commit f7f4232a4a830e445b3c46d0b25ba01dcbfef82f
Author: Adrian Cole <ac...@pivotal.io>
AuthorDate: Wed May 8 15:05:28 2019 +0800

    Ports all zipkin-server tests to Kotlin
    
    This reduces the amount of code to maintain and helps us learn Kotlin.
---
 .../server/internal/ZipkinHttpCollector.java       |   4 +
 .../java/zipkin/server/ITEnableZipkinServer.java   |  63 ----
 ...ZipkinKafkaCollectorPropertiesOverrideTest.java |  81 -----
 ...kinRabbitMQCollectorPropertiesOverrideTest.java |  89 -----
 ...nElasticsearchStorageAutoConfigurationTest.java | 400 ---------------------
 .../server/internal/ITZipkinMetricsHealth.java     | 238 ------------
 .../internal/ITZipkinMetricsHealthDirty.java       | 131 -------
 .../zipkin2/server/internal/ITZipkinServer.java    | 180 ----------
 .../internal/ITZipkinServerAutocomplete.java       |  94 -----
 .../server/internal/ITZipkinServerCORS.java        | 124 -------
 .../ITZipkinServerHttpCollectorDisabled.java       |  69 ----
 .../internal/ITZipkinServerQueryDisabled.java      |  63 ----
 .../zipkin2/server/internal/ITZipkinServerSsl.java |  84 -----
 .../internal/ZipkinServerConfigurationTest.java    | 155 --------
 .../server/internal/brave/ITZipkinSelfTracing.java | 101 ------
 .../elasticsearch/BasicAuthInterceptorTest.java    |  63 ----
 .../ZipkinKafkaCollectorConfigurationTest.java     | 107 ------
 .../kafka/ZipkinKafkaCollectorPropertiesTest.java  |  34 --
 .../ZipkinRabbitMQCollectorConfigurationTest.java  | 112 ------
 .../ZipkinRabbitMQCollectorPropertiesTest.java     |  57 ---
 .../internal/ui/ITZipkinUiConfiguration.java       | 160 ---------
 .../internal/ui/ZipkinUiConfigurationTest.java     | 214 -----------
 ...ipkinCassandraStorageAutoConfigurationTest.java | 154 --------
 .../ZipkinCassandraStorageConfigurationTest.java   | 140 --------
 .../v1/ZipkinMySQLStorageConfigurationTest.java    | 158 --------
 .../kotlin/zipkin/server/ITEnableZipkinServer.kt   |  46 +++
 .../ZipkinKafkaCollectorPropertiesOverrideTest.kt  |  72 ++++
 ...ipkinRabbitMQCollectorPropertiesOverrideTest.kt |  83 +++++
 ...kinElasticsearchStorageAutoConfigurationTest.kt | 360 +++++++++++++++++++
 .../test/kotlin/zipkin2/server/internal/Http.kt    |  66 ++++
 .../server/internal/ITZipkinGrpcCollector.kt       |  17 +-
 .../server/internal/ITZipkinHttpCollector.kt       | 150 +++-----
 .../server/internal/ITZipkinMetricsHealth.kt       | 195 ++++++++++
 .../server/internal/ITZipkinMetricsHealthDirty.kt  |  88 +++++
 .../zipkin2/server/internal/ITZipkinServer.kt      | 156 ++++++++
 .../server/internal/ITZipkinServerAutocomplete.kt  |  72 ++++
 .../zipkin2/server/internal/ITZipkinServerCORS.kt  | 106 ++++++
 .../ITZipkinServerHttpCollectorDisabled.kt         |  53 +++
 .../server/internal/ITZipkinServerQueryDisabled.kt |  52 +++
 .../zipkin2/server/internal/ITZipkinServerSsl.kt   |  75 ++++
 .../internal/InMemoryCollectorConfiguration.kt     |  23 ++
 .../internal/ZipkinServerConfigurationTest.kt      | 150 ++++++++
 .../server/internal/brave/ITZipkinSelfTracing.kt   |  67 ++++
 .../zipkin2/server/internal/cassandra/Access.kt}   |  18 +-
 .../zipkin2/server/internal/cassandra3/Access.kt}  |  18 +-
 .../server/internal/elasticsearch/Access.kt}       |  18 +-
 .../elasticsearch/BasicAuthInterceptorTest.kt      |  44 +++
 .../zipkin2/server/internal/kafka/Access.kt}       |  36 +-
 .../kafka/ZipkinKafkaCollectorConfigurationTest.kt |  67 ++++
 .../kafka/ZipkinKafkaCollectorPropertiesTest.kt}   |  21 +-
 .../zipkin2/server/internal/mysql/Access.kt}       |  19 +-
 .../ZipkinPrometheusMetricsConfigurationTest.kt}   |  78 ++--
 .../zipkin2/server/internal/rabbitmq/Access.kt}    |  36 +-
 .../ZipkinRabbitMQCollectorConfigurationTest.kt    |  70 ++++
 .../ZipkinRabbitMQCollectorPropertiesTest.kt       |  53 +++
 .../server/internal/ui/ITZipkinUiConfiguration.kt  | 124 +++++++
 .../internal/ui/ZipkinUiConfigurationTest.kt       | 185 ++++++++++
 .../ZipkinCassandraStorageAutoConfigurationTest.kt | 125 +++++++
 .../v1/ZipkinCassandraStorageConfigurationTest.kt  | 114 ++++++
 .../v1/ZipkinMySQLStorageConfigurationTest.kt      | 130 +++++++
 60 files changed, 2752 insertions(+), 3310 deletions(-)

diff --git a/zipkin-server/src/main/java/zipkin2/server/internal/ZipkinHttpCollector.java b/zipkin-server/src/main/java/zipkin2/server/internal/ZipkinHttpCollector.java
index 251c641..897f63e 100644
--- a/zipkin-server/src/main/java/zipkin2/server/internal/ZipkinHttpCollector.java
+++ b/zipkin-server/src/main/java/zipkin2/server/internal/ZipkinHttpCollector.java
@@ -185,6 +185,10 @@ final class UnzippingBytesRequestConverter implements RequestConverterFunction {
     }
 
     if (content.isEmpty()) ZipkinHttpCollector.maybeLog("Empty POST body", ctx, request);
+    if (content.length() == 2 && "[]".equals(content.toStringAscii())) {
+      ZipkinHttpCollector.maybeLog("Empty JSON list POST body", ctx, request);
+      content = HttpData.EMPTY_DATA;
+    }
 
     byte[] result = content.array();
     ZipkinHttpCollector.metrics.incrementBytes(result.length);
diff --git a/zipkin-server/src/test/java/zipkin/server/ITEnableZipkinServer.java b/zipkin-server/src/test/java/zipkin/server/ITEnableZipkinServer.java
deleted file mode 100644
index b080743..0000000
--- a/zipkin-server/src/test/java/zipkin/server/ITEnableZipkinServer.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin.server;
-
-import com.linecorp.armeria.server.Server;
-import java.io.IOException;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.Response;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.junit4.SpringRunner;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static zipkin2.server.internal.ITZipkinServer.url;
-
-@SpringBootTest(
-  classes = CustomServer.class,
-  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
-  properties = "spring.config.name=zipkin-server"
-)
-@RunWith(SpringRunner.class)
-public class ITEnableZipkinServer {
-
-  @Autowired Server server;
-
-  OkHttpClient client = new OkHttpClient.Builder().followRedirects(false).build();
-
-  @Test public void writeSpans_noContentTypeIsJson() throws Exception {
-    Response response = get("/api/v2/services");
-
-    assertThat(response.code())
-      .isEqualTo(200);
-  }
-
-  Response get(String path) throws IOException {
-    return client.newCall(new Request.Builder()
-      .url(url(server, path))
-      .build()).execute();
-  }
-}
-@SpringBootApplication
-@EnableZipkinServer
-class CustomServer {
-
-}
diff --git a/zipkin-server/src/test/java/zipkin2/collector/kafka/ZipkinKafkaCollectorPropertiesOverrideTest.java b/zipkin-server/src/test/java/zipkin2/collector/kafka/ZipkinKafkaCollectorPropertiesOverrideTest.java
deleted file mode 100644
index 34990ff..0000000
--- a/zipkin-server/src/test/java/zipkin2/collector/kafka/ZipkinKafkaCollectorPropertiesOverrideTest.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.collector.kafka;
-
-import java.util.Arrays;
-import java.util.function.Function;
-import org.assertj.core.api.Assertions;
-import org.junit.After;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.springframework.boot.test.util.TestPropertyValues;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
-import zipkin2.server.internal.kafka.Access;
-
-@RunWith(Parameterized.class)
-public class ZipkinKafkaCollectorPropertiesOverrideTest {
-
-  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
-
-  @After
-  public void close() {
-    if (context != null) context.close();
-  }
-
-  @Parameterized.Parameter(0)
-  public String property;
-
-  @Parameterized.Parameter(1)
-  public Object value;
-
-  @Parameterized.Parameter(2)
-  public Function<KafkaCollector.Builder, Object> builderExtractor;
-
-  @Parameterized.Parameters(name = "{0}")
-  public static Iterable<Object[]> data() {
-    return Arrays.asList(
-        parameters(
-            "bootstrap-servers",
-            "127.0.0.1:9092",
-            b -> b.properties.getProperty("bootstrap.servers")),
-        parameters("group-id", "zapkin", b -> b.properties.getProperty("group.id")),
-        parameters("topic", "zapkin", b -> b.topic),
-        parameters("streams", 2, b -> b.streams),
-        parameters(
-            "overrides.auto.offset.reset",
-            "latest",
-            b -> b.properties.getProperty("auto.offset.reset")));
-  }
-
-  /** to allow us to define with a lambda */
-  static <T> Object[] parameters(
-      String propertySuffix, T value, Function<KafkaCollector.Builder, T> builderExtractor) {
-    return new Object[] {"zipkin.collector.kafka." + propertySuffix, value, builderExtractor};
-  }
-
-  @Test
-  public void propertyTransferredToCollectorBuilder() {
-    TestPropertyValues.of(property + ":" + value).applyTo(context);
-    Access.registerKafkaProperties(context);
-    context.refresh();
-
-    Assertions.assertThat(Access.collectorBuilder(context))
-        .extracting(builderExtractor)
-        .isEqualTo(value);
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/collector/rabbitmq/ZipkinRabbitMQCollectorPropertiesOverrideTest.java b/zipkin-server/src/test/java/zipkin2/collector/rabbitmq/ZipkinRabbitMQCollectorPropertiesOverrideTest.java
deleted file mode 100644
index 4084ce1..0000000
--- a/zipkin-server/src/test/java/zipkin2/collector/rabbitmq/ZipkinRabbitMQCollectorPropertiesOverrideTest.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.collector.rabbitmq;
-
-import java.net.URI;
-import java.util.Arrays;
-import java.util.function.Function;
-import org.junit.After;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.springframework.boot.test.util.TestPropertyValues;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
-import zipkin2.server.internal.rabbitmq.Access;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-@RunWith(Parameterized.class)
-public class ZipkinRabbitMQCollectorPropertiesOverrideTest {
-
-  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
-
-  @After
-  public void close() {
-    if (context != null) context.close();
-  }
-
-  @Parameterized.Parameter(0)
-  public String property;
-
-  @Parameterized.Parameter(1)
-  public Object value;
-
-  @Parameterized.Parameter(2)
-  public Function<RabbitMQCollector.Builder, Object> builderExtractor;
-
-  @Parameterized.Parameters(name = "{0}")
-  public static Iterable<Object[]> data() {
-    return Arrays.asList(
-        // intentionally punting on comma-separated form of a list of addresses as it doesn't fit
-        // this unit test. Better to make a separate one than force-fit!
-        parameters("addresses", "localhost:5671", builder -> builder.addresses[0].toString()),
-        parameters("concurrency", 2, builder -> builder.concurrency),
-        parameters(
-            "connectionTimeout",
-            30_000,
-            builder -> builder.connectionFactory.getConnectionTimeout()),
-        parameters("password", "admin", builder -> builder.connectionFactory.getPassword()),
-        parameters("queue", "zapkin", builder -> builder.queue),
-        parameters("username", "admin", builder -> builder.connectionFactory.getUsername()),
-        parameters("virtualHost", "/hello", builder -> builder.connectionFactory.getVirtualHost()),
-        parameters("useSsl", true, builder -> builder.connectionFactory.isSSL()),
-        parameters(
-            "uri",
-            URI.create("amqp://localhost"),
-            builder -> URI.create("amqp://" + builder.connectionFactory.getHost())));
-  }
-
-  /** to allow us to define with a lambda */
-  static <T> Object[] parameters(
-      String propertySuffix, T value, Function<RabbitMQCollector.Builder, T> builderExtractor) {
-    return new Object[] {"zipkin.collector.rabbitmq." + propertySuffix, value, builderExtractor};
-  }
-
-  @Test
-  public void propertyTransferredToCollectorBuilder() throws Exception {
-    TestPropertyValues.of(property + ":" + value).applyTo(context);
-    Access.registerRabbitMQProperties(context);
-    context.refresh();
-
-    assertThat(Access.collectorBuilder(context))
-        .extracting(builderExtractor)
-        .isEqualTo(value);
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/elasticsearch/ZipkinElasticsearchStorageAutoConfigurationTest.java b/zipkin-server/src/test/java/zipkin2/elasticsearch/ZipkinElasticsearchStorageAutoConfigurationTest.java
deleted file mode 100644
index d9cfa8e..0000000
--- a/zipkin-server/src/test/java/zipkin2/elasticsearch/ZipkinElasticsearchStorageAutoConfigurationTest.java
+++ /dev/null
@@ -1,400 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.elasticsearch;
-
-import java.util.concurrent.TimeUnit;
-import okhttp3.Interceptor;
-import okhttp3.OkHttpClient;
-import org.junit.After;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.springframework.beans.factory.BeanCreationException;
-import org.springframework.beans.factory.NoSuchBeanDefinitionException;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.boot.test.util.TestPropertyValues;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import zipkin2.server.internal.elasticsearch.Access;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class ZipkinElasticsearchStorageAutoConfigurationTest {
-
-  @Rule public ExpectedException thrown = ExpectedException.none();
-
-  final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
-
-  @After
-  public void close() {
-    context.close();
-  }
-
-  @Test
-  public void doesntProvideStorageComponent_whenStorageTypeNotElasticsearch() {
-    TestPropertyValues.of("zipkin.storage.type:cassandra").applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    thrown.expect(NoSuchBeanDefinitionException.class);
-    es();
-  }
-
-  @Test
-  public void providesStorageComponent_whenStorageTypeElasticsearchAndHostsAreUrls() {
-    TestPropertyValues.of(
-        "zipkin.storage.type:elasticsearch",
-        "zipkin.storage.elasticsearch.hosts:http://host1:9200")
-      .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    assertThat(es()).isNotNull();
-  }
-
-  @Test
-  public void canOverridesProperty_hostsWithList() {
-    TestPropertyValues.of(
-        "zipkin.storage.type:elasticsearch",
-        "zipkin.storage.elasticsearch.hosts:http://host1:9200,http://host2:9200")
-      .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    assertThat(es().hostsSupplier().get())
-        .containsExactly("http://host1:9200", "http://host2:9200");
-  }
-
-  @Test
-  public void configuresPipeline() {
-    TestPropertyValues.of(
-        "zipkin.storage.type:elasticsearch",
-        "zipkin.storage.elasticsearch.hosts:http://host1:9200",
-        "zipkin.storage.elasticsearch.pipeline:zipkin")
-      .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    assertThat(es().pipeline()).isEqualTo("zipkin");
-  }
-
-  @Test
-  public void configuresMaxRequests() {
-    TestPropertyValues.of(
-        "zipkin.storage.type:elasticsearch",
-        "zipkin.storage.elasticsearch.hosts:http://host1:9200",
-        "zipkin.storage.elasticsearch.max-requests:200")
-      .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    assertThat(es().maxRequests()).isEqualTo(200);
-  }
-
-  /** This helps ensure old setups don't break (provided they have http port 9200 open) */
-  @Test
-  public void coersesPort9300To9200() {
-    TestPropertyValues.of(
-        "zipkin.storage.type:elasticsearch",
-        "zipkin.storage.elasticsearch.hosts:host1:9300")
-      .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    assertThat(es().hostsSupplier().get()).containsExactly("http://host1:9200");
-  }
-
-  @Test
-  public void httpPrefixOptional() {
-    TestPropertyValues.of(
-        "zipkin.storage.type:elasticsearch",
-        "zipkin.storage.elasticsearch.hosts:host1:9200")
-      .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    assertThat(es().hostsSupplier().get()).containsExactly("http://host1:9200");
-  }
-
-  @Test
-  public void defaultsToPort9200() {
-    TestPropertyValues.of(
-        "zipkin.storage.type:elasticsearch",
-        "zipkin.storage.elasticsearch.hosts:host1")
-      .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    assertThat(es().hostsSupplier().get()).containsExactly("http://host1:9200");
-  }
-
-  @Configuration
-  static class InterceptorConfiguration {
-
-    static Interceptor one = chain -> null;
-    static Interceptor two = chain -> null;
-
-    @Bean
-    @Qualifier("zipkinElasticsearchHttp")
-    Interceptor one() {
-      return one;
-    }
-
-    @Bean
-    @Qualifier("zipkinElasticsearchHttp")
-    Interceptor two() {
-      return two;
-    }
-  }
-
-  /** Ensures we can wire up network interceptors, such as for logging or authentication */
-  @Test
-  public void usesInterceptorsQualifiedWith_zipkinElasticsearchHttp() {
-    TestPropertyValues.of(
-      "zipkin.storage.type:elasticsearch",
-      "zipkin.storage.elasticsearch.hosts:host1:9200")
-      .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.register(InterceptorConfiguration.class);
-    context.refresh();
-
-    assertThat(context.getBean(OkHttpClient.class).networkInterceptors())
-        .containsOnlyOnce(InterceptorConfiguration.one, InterceptorConfiguration.two);
-  }
-
-  @Test
-  public void timeout_defaultsTo10Seconds() {
-    TestPropertyValues.of(
-      "zipkin.storage.type:elasticsearch",
-      "zipkin.storage.elasticsearch.hosts:host1:9200")
-      .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    OkHttpClient client = context.getBean(OkHttpClient.class);
-    assertThat(client.connectTimeoutMillis()).isEqualTo(10_000);
-    assertThat(client.readTimeoutMillis()).isEqualTo(10_000);
-    assertThat(client.writeTimeoutMillis()).isEqualTo(10_000);
-  }
-
-  @Test
-  public void timeout_override() {
-    int timeout = 30_000;
-    TestPropertyValues.of(
-        "zipkin.storage.type:elasticsearch",
-        "zipkin.storage.elasticsearch.hosts:http://host1:9200",
-        "zipkin.storage.elasticsearch.timeout:" + timeout)
-      .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    OkHttpClient client = context.getBean(OkHttpClient.class);
-    assertThat(client.connectTimeoutMillis()).isEqualTo(timeout);
-    assertThat(client.readTimeoutMillis()).isEqualTo(timeout);
-    assertThat(client.writeTimeoutMillis()).isEqualTo(timeout);
-  }
-
-  @Test
-  public void strictTraceId_defaultsToTrue() {
-    TestPropertyValues.of(
-      "zipkin.storage.type:elasticsearch",
-      "zipkin.storage.elasticsearch.hosts:http://host1:9200")
-      .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-    assertThat(es().strictTraceId()).isTrue();
-  }
-
-  @Test
-  public void strictTraceId_canSetToFalse() {
-    TestPropertyValues.of(
-        "zipkin.storage.type:elasticsearch",
-        "zipkin.storage.elasticsearch.hosts:http://host1:9200",
-        "zipkin.storage.strict-trace-id:false")
-    .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    assertThat(es().strictTraceId()).isFalse();
-  }
-
-  @Test
-  public void dailyIndexFormat() {
-    TestPropertyValues.of(
-        "zipkin.storage.type:elasticsearch",
-        "zipkin.storage.elasticsearch.hosts:http://host1:9200")
-    .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    assertThat(es().indexNameFormatter().formatTypeAndTimestamp("span", 0))
-        .isEqualTo("zipkin*span-1970-01-01");
-  }
-
-  @Test
-  public void dailyIndexFormat_overridingPrefix() {
-    TestPropertyValues.of(
-        "zipkin.storage.type:elasticsearch",
-        "zipkin.storage.elasticsearch.hosts:http://host1:9200",
-        "zipkin.storage.elasticsearch.index:zipkin_prod")
-    .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    assertThat(es().indexNameFormatter().formatTypeAndTimestamp("span", 0))
-        .isEqualTo("zipkin_prod*span-1970-01-01");
-  }
-
-  @Test
-  public void dailyIndexFormat_overridingDateSeparator() {
-    TestPropertyValues.of(
-        "zipkin.storage.type:elasticsearch",
-        "zipkin.storage.elasticsearch.hosts:http://host1:9200",
-        "zipkin.storage.elasticsearch.date-separator:.")
-    .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    assertThat(es().indexNameFormatter().formatTypeAndTimestamp("span", 0))
-        .isEqualTo("zipkin*span-1970.01.01");
-  }
-
-  @Test
-  public void dailyIndexFormat_overridingDateSeparator_empty() {
-    TestPropertyValues.of(
-        "zipkin.storage.type:elasticsearch",
-        "zipkin.storage.elasticsearch.hosts:http://host1:9200",
-        "zipkin.storage.elasticsearch.date-separator:")
-    .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    assertThat(es().indexNameFormatter().formatTypeAndTimestamp("span", 0))
-        .isEqualTo("zipkin*span-19700101");
-  }
-
-  @Test
-  public void dailyIndexFormat_overridingDateSeparator_invalidToBeMultiChar() {
-    TestPropertyValues.of(
-        "zipkin.storage.type:elasticsearch",
-        "zipkin.storage.elasticsearch.hosts:http://host1:9200",
-        "zipkin.storage.elasticsearch.date-separator:blagho")
-    .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-
-    thrown.expect(BeanCreationException.class);
-    context.refresh();
-  }
-
-  @Test
-  public void namesLookbackAssignedFromQueryLookback() {
-    TestPropertyValues.of(
-        "zipkin.storage.type:elasticsearch",
-        "zipkin.storage.elasticsearch.hosts:http://host1:9200",
-        "zipkin.query.lookback:" + TimeUnit.DAYS.toMillis(2))
-    .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    assertThat(es().namesLookback()).isEqualTo((int) TimeUnit.DAYS.toMillis(2));
-  }
-
-  @Test
-  public void doesntProvideBasicAuthInterceptor_whenBasicAuthUserNameandPasswordNotConfigured() {
-    TestPropertyValues.of(
-        "zipkin.storage.type:elasticsearch",
-        "zipkin.storage.elasticsearch.hosts:http://host1:9200")
-    .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    thrown.expect(NoSuchBeanDefinitionException.class);
-    context.getBean(Interceptor.class);
-  }
-
-  @Test
-  public void providesBasicAuthInterceptor_whenBasicAuthUserNameAndPasswordConfigured() {
-    TestPropertyValues.of(
-        "zipkin.storage.type:elasticsearch",
-        "zipkin.storage.elasticsearch.hosts:http://host1:9200",
-        "zipkin.storage.elasticsearch.username:somename",
-        "zipkin.storage.elasticsearch.password:pass")
-    .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    assertThat(context.getBean(OkHttpClient.class).networkInterceptors())
-        .extracting(i -> i.getClass().getName())
-        .contains("zipkin2.server.internal.elasticsearch.BasicAuthInterceptor");
-  }
-
-  @Test
-  public void searchEnabled_false() {
-    TestPropertyValues.of(
-        "zipkin.storage.type:elasticsearch",
-        "zipkin.storage.search-enabled:false")
-    .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    assertThat(context.getBean(ElasticsearchStorage.class).searchEnabled()).isFalse();
-  }
-
-  @Test
-  public void autocompleteKeys_list() {
-    TestPropertyValues.of(
-      "zipkin.storage.type:elasticsearch",
-      "zipkin.storage.autocomplete-keys:environment")
-      .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    assertThat(context.getBean(ElasticsearchStorage.class).autocompleteKeys())
-      .containsOnly("environment");
-  }
-
-  @Test
-  public void autocompleteTtl() {
-    TestPropertyValues.of(
-      "zipkin.storage.type:elasticsearch",
-      "zipkin.storage.autocomplete-ttl:60000")
-      .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    assertThat(context.getBean(ElasticsearchStorage.class).autocompleteTtl())
-      .isEqualTo(60000);
-  }
-
-  @Test
-  public void autocompleteCardinality() {
-    TestPropertyValues.of(
-      "zipkin.storage.type:elasticsearch",
-      "zipkin.storage.autocomplete-cardinality:5000")
-      .applyTo(context);
-    Access.registerElasticsearchHttp(context);
-    context.refresh();
-
-    assertThat(context.getBean(ElasticsearchStorage.class).autocompleteCardinality())
-      .isEqualTo(5000);
-  }
-
-  ElasticsearchStorage es() {
-    return context.getBean(ElasticsearchStorage.class);
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinMetricsHealth.java b/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinMetricsHealth.java
deleted file mode 100644
index ab92295..0000000
--- a/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinMetricsHealth.java
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.server.internal;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.jayway.jsonpath.JsonPath;
-import com.linecorp.armeria.server.Server;
-import io.micrometer.prometheus.PrometheusMeterRegistry;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.junit4.SpringRunner;
-import zipkin.server.ZipkinServer;
-import zipkin2.Span;
-import zipkin2.codec.SpanBytesEncoder;
-import zipkin2.storage.InMemoryStorage;
-
-import static java.util.Arrays.asList;
-import static org.assertj.core.api.Assertions.assertThat;
-import static zipkin2.TestObjects.LOTS_OF_SPANS;
-import static zipkin2.server.internal.ITZipkinServer.url;
-
-@SpringBootTest(
-  classes = ZipkinServer.class,
-  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
-  properties = "spring.config.name=zipkin-server"
-)
-@RunWith(SpringRunner.class)
-public class ITZipkinMetricsHealth {
-
-  @Autowired InMemoryStorage storage;
-  @Autowired PrometheusMeterRegistry registry;
-  @Autowired Server server;
-
-  OkHttpClient client = new OkHttpClient.Builder().followRedirects(true).build();
-
-  @Before public void init() {
-    storage.clear();
-  }
-
-  @Test public void healthIsOK() throws Exception {
-    assertThat(get("/health").isSuccessful())
-      .isTrue();
-
-    // ensure we don't track health in prometheus
-    assertThat(scrape())
-      .doesNotContain("health");
-  }
-
-  @Test public void metricsIsOK() throws Exception {
-    assertThat(get("/metrics").isSuccessful())
-      .isTrue();
-
-    // ensure we don't track metrics in prometheus
-    assertThat(scrape())
-      .doesNotContain("metrics");
-  }
-
-  @Test public void actuatorIsOK() throws Exception {
-    assertThat(get("/actuator").isSuccessful())
-      .isTrue();
-
-    // ensure we don't track actuator in prometheus
-    assertThat(scrape())
-      .doesNotContain("actuator");
-  }
-
-  @Test public void prometheusIsOK() throws Exception {
-    assertThat(get("/prometheus").isSuccessful())
-      .isTrue();
-
-    // ensure we don't track prometheus, UI requests in prometheus
-    assertThat(scrape())
-      .doesNotContain("prometheus")
-      .doesNotContain("uri=\"/zipkin")
-      .doesNotContain("uri=\"/\"");
-  }
-
-  @Test public void notFound_prometheus() throws Exception {
-    assertThat(get("/doo-wop").isSuccessful())
-      .isFalse();
-
-    assertThat(scrape())
-      .contains("uri=\"NOT_FOUND\"")
-      .doesNotContain("uri=\"/doo-wop");
-  }
-
-  @Test public void redirected_prometheus() throws Exception {
-    assertThat(get("/").isSuccessful())
-      .isTrue(); // follows redirects
-
-    assertThat(scrape())
-      .contains("uri=\"REDIRECTION\"")
-      .contains("uri=\"/zipkin/index.html\"")
-      .doesNotContain("uri=\"/\"");
-  }
-
-  @Test public void apiTemplate_prometheus() throws Exception {
-    List<Span> spans = asList(LOTS_OF_SPANS[0]);
-    byte[] body = SpanBytesEncoder.JSON_V2.encodeList(spans);
-    assertThat(post("/api/v2/spans", body).isSuccessful())
-      .isTrue();
-
-    assertThat(get("/api/v2/trace/" + LOTS_OF_SPANS[0].traceId()).isSuccessful())
-      .isTrue();
-
-    assertThat(scrape())
-      .contains("uri=\"/api/v2/trace/{traceId}\"")
-      .doesNotContain(LOTS_OF_SPANS[0].traceId());
-  }
-
-  @Test public void forwardedRoute_prometheus() throws Exception {
-    assertThat(get("/zipkin/api/v2/services").isSuccessful())
-      .isTrue();
-
-    assertThat(scrape())
-      .contains("uri=\"/api/v2/services\"")
-      .doesNotContain("uri=\"/zipkin/api/v2/services\"");
-  }
-
-  String scrape() throws InterruptedException {
-    Thread.sleep(100);
-    return registry.scrape();
-  }
-
-  /** Makes sure the prometheus filter doesn't count twice */
-  @Test public void writeSpans_updatesPrometheusMetrics() throws Exception {
-    List<Span> spans = asList(LOTS_OF_SPANS[0], LOTS_OF_SPANS[1], LOTS_OF_SPANS[2]);
-    byte[] body = SpanBytesEncoder.JSON_V2.encodeList(spans);
-
-    post("/api/v2/spans", body);
-    post("/api/v2/spans", body);
-
-    Thread.sleep(100); // sometimes travis flakes getting the "http.server.requests" timer
-    double messagesCount = registry.counter("zipkin_collector.spans", "transport", "http").count();
-    // Get the http count from the registry and it should match the summation previous count
-    // and count of calls below
-    long httpCount = registry
-      .find("http.server.requests")
-      .tag("uri", "/api/v2/spans")
-      .timer()
-      .count();
-
-    // ensure unscoped counter does not exist
-    assertThat(scrape())
-      .doesNotContain("zipkin_collector_spans_total " + messagesCount)
-      .contains("zipkin_collector_spans_total{transport=\"http\",} " + messagesCount)
-      .contains(
-        "http_server_requests_seconds_count{method=\"POST\",status=\"202\",uri=\"/api/v2/spans\",} "
-          + httpCount);
-  }
-
-  @Test public void readsHealth() throws Exception {
-    String json = getAsString("/health");
-    assertThat(readString(json, "$.status"))
-      .isIn("UP", "DOWN", "UNKNOWN");
-    assertThat(readString(json, "$.zipkin.status"))
-      .isIn("UP", "DOWN", "UNKNOWN");
-  }
-
-  @Test public void writesSpans_readMetricsFormat() throws Exception {
-    byte[] span = {'z', 'i', 'p', 'k', 'i', 'n'};
-    List<Span> spans = asList(LOTS_OF_SPANS[0], LOTS_OF_SPANS[1], LOTS_OF_SPANS[2]);
-    byte[] body = SpanBytesEncoder.JSON_V2.encodeList(spans);
-    post("/api/v2/spans", body);
-    post("/api/v2/spans", body);
-    post("/api/v2/spans", span);
-    Thread.sleep(1500);
-
-    String metrics = getAsString("/metrics");
-
-    assertThat(readJson(metrics))
-      .containsExactlyInAnyOrder(
-        "gauge.zipkin_collector.message_spans.http"
-        , "gauge.zipkin_collector.message_bytes.http"
-        , "counter.zipkin_collector.messages.http"
-        , "counter.zipkin_collector.bytes.http"
-        , "counter.zipkin_collector.spans.http"
-        , "counter.zipkin_collector.messages_dropped.http"
-        , "counter.zipkin_collector.spans_dropped.http"
-      );
-  }
-
-  private String getAsString(String path) throws IOException {
-    Response response = get(path);
-    assertThat(response.isSuccessful())
-      .withFailMessage(response.toString())
-      .isTrue();
-    return response.body().string();
-  }
-
-  private Response get(String path) throws IOException {
-    return client.newCall(new Request.Builder().url(url(server, path)).build()).execute();
-  }
-
-  private Response post(String path, byte[] body) throws IOException {
-    return client.newCall(new Request.Builder()
-      .url(url(server, path))
-      .post(RequestBody.create(null, body))
-      .build()).execute();
-  }
-
-  static String readString(String json, String jsonPath) {
-    return JsonPath.compile(jsonPath).read(json);
-  }
-
-  static List readJson(String json) throws Exception {
-    ObjectMapper mapper = new ObjectMapper();
-    JsonNode jsonNode = mapper.readTree(json);
-    List<String> fieldsList = new ArrayList<>();
-    jsonNode.fieldNames().forEachRemaining(fieldsList::add);
-    return fieldsList;
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinMetricsHealthDirty.java b/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinMetricsHealthDirty.java
deleted file mode 100644
index 3888342..0000000
--- a/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinMetricsHealthDirty.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.server.internal;
-
-import com.jayway.jsonpath.JsonPath;
-import com.linecorp.armeria.server.Server;
-import io.micrometer.prometheus.PrometheusMeterRegistry;
-import java.io.IOException;
-import java.util.List;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.annotation.DirtiesContext;
-import org.springframework.test.context.junit4.SpringRunner;
-import zipkin.server.ZipkinServer;
-import zipkin2.Span;
-import zipkin2.codec.SpanBytesEncoder;
-import zipkin2.storage.InMemoryStorage;
-
-import static java.util.Arrays.asList;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.springframework.test.annotation.DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD;
-import static zipkin2.TestObjects.LOTS_OF_SPANS;
-import static zipkin2.server.internal.ITZipkinServer.url;
-
-/**
- * We cannot clear the micrometer registry easily, so we have recreate the spring context. This is
- * extremely slow, so please only add tests that require isolation here.
- */
-@SpringBootTest(
-  classes = ZipkinServer.class,
-  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
-  properties = "spring.config.name=zipkin-server"
-)
-@RunWith(SpringRunner.class)
-@DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD)
-public class ITZipkinMetricsHealthDirty {
-
-  @Autowired InMemoryStorage storage;
-  @Autowired PrometheusMeterRegistry registry;
-  @Autowired Server server;
-
-  OkHttpClient client = new OkHttpClient.Builder().followRedirects(true).build();
-
-  @Before public void init() {
-    storage.clear();
-  }
-
-  @Test public void writeSpans_updatesMetrics() throws Exception {
-    List<Span> spans = asList(LOTS_OF_SPANS[0], LOTS_OF_SPANS[1], LOTS_OF_SPANS[2]);
-    byte[] body = SpanBytesEncoder.JSON_V2.encodeList(spans);
-    double messagesCount =
-      registry.counter("zipkin_collector.messages", "transport", "http").count();
-    double bytesCount = registry.counter("zipkin_collector.bytes", "transport", "http").count();
-    double spansCount = registry.counter("zipkin_collector.spans", "transport", "http").count();
-    post("/api/v2/spans", body);
-    post("/api/v2/spans", body);
-
-    String json = getAsString("/metrics");
-
-    assertThat(readDouble(json, "$.['counter.zipkin_collector.messages.http']"))
-      .isEqualTo(messagesCount + 2.0);
-    assertThat(readDouble(json, "$.['counter.zipkin_collector.bytes.http']"))
-      .isEqualTo(bytesCount + (body.length * 2));
-    assertThat(readDouble(json, "$.['gauge.zipkin_collector.message_bytes.http']"))
-      .isEqualTo(body.length);
-    assertThat(readDouble(json, "$.['counter.zipkin_collector.spans.http']"))
-      .isEqualTo(spansCount + (spans.size() * 2));
-    assertThat(readDouble(json, "$.['gauge.zipkin_collector.message_spans.http']"))
-      .isEqualTo(spans.size());
-  }
-
-  @Test public void writeSpans_malformedUpdatesMetrics() throws Exception {
-    byte[] body = {'h', 'e', 'l', 'l', 'o'};
-    Double messagesCount =
-      registry.counter("zipkin_collector.messages", "transport", "http").count();
-    Double messagesDroppedCount =
-      registry.counter("zipkin_collector.messages_dropped", "transport", "http").count();
-    post("/api/v2/spans", body);
-
-    String json = getAsString("/metrics");
-
-    assertThat(readDouble(json, "$.['counter.zipkin_collector.messages.http']"))
-      .isEqualTo(messagesCount + 1);
-    assertThat(readDouble(json, "$.['counter.zipkin_collector.messages_dropped.http']"))
-      .isEqualTo(messagesDroppedCount + 1);
-  }
-
-  private String getAsString(String path) throws IOException {
-    Response response = get(path);
-    assertThat(response.isSuccessful())
-      .withFailMessage(response.toString())
-      .isTrue();
-    return response.body().string();
-  }
-
-  private Response get(String path) throws IOException {
-    return client.newCall(new Request.Builder().url(url(server, path)).build()).execute();
-  }
-
-  private Response post(String path, byte[] body) throws IOException {
-    return client.newCall(new Request.Builder()
-      .url(url(server, path))
-      .post(RequestBody.create(null, body))
-      .build()).execute();
-  }
-
-  static Double readDouble(String json, String jsonPath) {
-    return JsonPath.compile(jsonPath).read(json);
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServer.java b/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServer.java
deleted file mode 100644
index d984fd8..0000000
--- a/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServer.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.server.internal;
-
-import com.linecorp.armeria.server.Server;
-import java.io.IOException;
-import java.util.List;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.Response;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.junit4.SpringRunner;
-import zipkin.server.ZipkinServer;
-import zipkin2.Endpoint;
-import zipkin2.Span;
-import zipkin2.TestObjects;
-import zipkin2.codec.SpanBytesEncoder;
-import zipkin2.storage.InMemoryStorage;
-
-import static java.util.Arrays.asList;
-import static org.assertj.core.api.Assertions.assertThat;
-import static zipkin2.TestObjects.TODAY;
-import static zipkin2.TestObjects.UTF_8;
-
-@SpringBootTest(
-  classes = ZipkinServer.class,
-  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
-  properties = "spring.config.name=zipkin-server"
-)
-@RunWith(SpringRunner.class)
-public class ITZipkinServer {
-  static final List<Span> TRACE = asList(TestObjects.CLIENT_SPAN);
-
-  @Autowired InMemoryStorage storage;
-  @Autowired Server server;
-
-  OkHttpClient client = new OkHttpClient.Builder().followRedirects(true).build();
-
-  @Before public void init() {
-    storage.clear();
-  }
-
-  @Test public void getTrace() throws Exception {
-    storage.accept(TRACE).execute();
-
-    Response response = get("/api/v2/trace/" + TRACE.get(0).traceId());
-    assertThat(response.isSuccessful()).isTrue();
-
-    assertThat(response.body().bytes())
-      .containsExactly(SpanBytesEncoder.JSON_V2.encodeList(TRACE));
-  }
-
-  @Test public void tracesQueryRequiresNoParameters() throws Exception {
-    storage.accept(TRACE).execute();
-    
-    Response response = get("/api/v2/traces");
-    assertThat(response.isSuccessful()).isTrue();
-    assertThat(response.body().string())
-      .isEqualTo("[" + new String(SpanBytesEncoder.JSON_V2.encodeList(TRACE), UTF_8) + "]");
-  }
-
-  @Test public void v2WiresUp() throws Exception {
-    assertThat(get("/api/v2/services").isSuccessful())
-      .isTrue();
-  }
-
-  @Test public void doesntSetCacheControlOnNameEndpointsWhenLessThan4Services() throws Exception {
-    storage.accept(TRACE).execute();
-
-    assertThat(get("/api/v2/services").header("Cache-Control"))
-      .isNull();
-
-    assertThat(get("/api/v2/spans?serviceName=web").header("Cache-Control"))
-      .isNull();
-
-    assertThat(get("/api/v2/remoteServices?serviceName=web").header("Cache-Control"))
-      .isNull();
-  }
-
-  @Test public void spanNameQueryWorksWithNonAsciiServiceName() throws Exception {
-    assertThat(get("/api/v2/spans?serviceName=个人信息服务").code())
-      .isEqualTo(200);
-  }
-
-  @Test public void remoteServiceNameQueryWorksWithNonAsciiServiceName() throws Exception {
-    assertThat(get("/api/v2/remoteServices?serviceName=个人信息服务").code())
-      .isEqualTo(200);
-  }
-
-  @Test public void setsCacheControlOnNameEndpointsWhenMoreThan3Services() throws Exception {
-    List<String> services = asList("foo", "bar", "baz", "quz");
-    for (int i = 0; i < services.size(); i++) {
-      storage.accept(asList(
-        Span.newBuilder().traceId("a").id(i + 1).timestamp(TODAY).name("whopper")
-          .localEndpoint(Endpoint.newBuilder().serviceName(services.get(i)).build())
-          .remoteEndpoint(Endpoint.newBuilder().serviceName(services.get(i) + 1).build())
-          .build()
-      )).execute();
-    }
-
-    assertThat(get("/api/v2/services").header("Cache-Control"))
-      .isEqualTo("max-age=300, must-revalidate");
-
-    assertThat(get("/api/v2/spans?serviceName=web").header("Cache-Control"))
-      .isEqualTo("max-age=300, must-revalidate");
-
-    assertThat(get("/api/v2/remoteServices?serviceName=web").header("Cache-Control"))
-      .isEqualTo("max-age=300, must-revalidate");
-
-    // Check that the response is alphabetically sorted.
-    assertThat(get("/api/v2/services").body().string())
-      .isEqualTo("[\"bar\",\"baz\",\"foo\",\"quz\"]");
-  }
-
-  @Test public void shouldAllowAnyOriginByDefault() throws Exception {
-    Response response = client.newCall(new Request.Builder()
-      .url(url(server, "/api/v2/traces"))
-      .header("Origin", "http://foo.example.com")
-      .build()).execute();
-
-    assertThat(response.isSuccessful()).isTrue();
-    assertThat(response.header("vary")).isNull();
-    assertThat(response.header("access-control-allow-credentials")).isNull();
-    assertThat(response.header("access-control-allow-origin")).contains("*");
-  }
-
-  @Test public void forwardsApiForUi() throws Exception {
-    assertThat(get("/zipkin/api/v2/traces").isSuccessful()).isTrue();
-    assertThat(get("/zipkin/api/v2/traces").isSuccessful()).isTrue();
-  }
-
-  /** Simulate a proxy which forwards / to zipkin as opposed to resolving / -> /zipkin first */
-  @Test public void redirectedHeaderUsesOriginalHostAndPort() throws Exception {
-    Request forwarded = new Request.Builder()
-      .url(url(server, "/"))
-      .addHeader("Host", "zipkin.com")
-      .addHeader("X-Forwarded-Proto", "https")
-      .addHeader("X-Forwarded-Port", "444")
-      .build();
-
-    Response response = client.newBuilder().followRedirects(false).build()
-      .newCall(forwarded).execute();
-
-    // Redirect header should be the proxy, not the backed IP/port
-    assertThat(response.header("Location"))
-      .isEqualTo("/zipkin/");
-  }
-
-  @Test public void infoEndpointIsAvailable() throws IOException {
-    assertThat(get("/info").isSuccessful()).isTrue();
-  }
-
-  private Response get(String path) throws IOException {
-    return client.newCall(new Request.Builder()
-      .url(url(server, path))
-      .build()).execute();
-  }
-
-  public static String url(Server server, String path) {
-    return "http://localhost:" + server.activePort().get().localAddress().getPort() + path;
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerAutocomplete.java b/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerAutocomplete.java
deleted file mode 100644
index 521df81..0000000
--- a/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerAutocomplete.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.server.internal;
-
-import com.linecorp.armeria.server.Server;
-import java.io.IOException;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.junit4.SpringRunner;
-import zipkin.server.ZipkinServer;
-import zipkin2.Span;
-import zipkin2.codec.SpanBytesEncoder;
-
-import static java.util.Arrays.asList;
-import static org.assertj.core.api.Assertions.assertThat;
-import static zipkin2.TestObjects.TODAY;
-import static zipkin2.server.internal.ITZipkinServer.url;
-
-/**
- * Integration test suite for autocomplete tags.
- *
- * Verifies that the whitelist of key can be configured via "zipkin.storage.autocomplete-keys".
- */
-@SpringBootTest(
-  classes = ZipkinServer.class,
-  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
-  properties = {
-    "spring.config.name=zipkin-server",
-    "zipkin.storage.autocomplete-keys=environment,clnt/finagle.version"
-  }
-)
-@RunWith(SpringRunner.class)
-public class ITZipkinServerAutocomplete {
-
-  @Autowired Server server;
-  OkHttpClient client = new OkHttpClient.Builder().followRedirects(false).build();
-
-  @Test public void setsCacheControlOnAutocompleteKeysEndpoint() throws Exception {
-    assertThat(get("/api/v2/autocompleteKeys").header("Cache-Control"))
-      .isEqualTo("max-age=300, must-revalidate");
-  }
-
-  @Test public void setsCacheControlOnAutocompleteEndpointWhenMoreThan3Values() throws Exception {
-    assertThat(get("/api/v2/autocompleteValues?key=environment").header("Cache-Control"))
-      .isNull();
-    assertThat(get("/api/v2/autocompleteValues?key=clnt/finagle.version").header("Cache-Control"))
-      .isNull();
-
-    for (int i = 0; i < 4; i++) {
-      post("/api/v2/spans", SpanBytesEncoder.JSON_V2.encodeList(asList(
-        Span.newBuilder().traceId("a").id(i + 1).timestamp(TODAY).name("whopper")
-          .putTag("clnt/finagle.version", "6.45." + i).build()
-      )));
-    }
-
-    assertThat(get("/api/v2/autocompleteValues?key=environment").header("Cache-Control"))
-      .isNull();
-    assertThat(get("/api/v2/autocompleteValues?key=clnt/finagle.version").header("Cache-Control"))
-      .isEqualTo("max-age=300, must-revalidate");
-  }
-
-  private Response get(String path) throws IOException {
-    return client.newCall(new Request.Builder()
-      .url(url(server, path))
-      .build()).execute();
-  }
-
-  private Response post(String path, byte[] body) throws IOException {
-    return client.newCall(new Request.Builder()
-      .url(url(server, path))
-      .post(RequestBody.create(null, body))
-      .build()).execute();
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerCORS.java b/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerCORS.java
deleted file mode 100644
index be5bb43..0000000
--- a/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerCORS.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.server.internal;
-
-import com.linecorp.armeria.server.Server;
-import java.io.IOException;
-import okhttp3.MediaType;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.junit4.SpringRunner;
-import zipkin.server.ZipkinServer;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static zipkin2.server.internal.ITZipkinServer.url;
-
-/**
- * Integration test suite for CORS configuration.
- *
- * Verifies that allowed-origins can be configured via properties (zipkin.query.allowed-origins).
- */
-@SpringBootTest(
-  classes = ZipkinServer.class,
-  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
-  properties = {
-    "spring.config.name=zipkin-server",
-    "zipkin.query.allowed-origins=" + ITZipkinServerCORS.ALLOWED_ORIGIN
-  }
-)
-@RunWith(SpringRunner.class)
-public class ITZipkinServerCORS {
-  static final String ALLOWED_ORIGIN = "http://foo.example.com";
-  static final String DISALLOWED_ORIGIN = "http://bar.example.com";
-
-  @Autowired Server server;
-  OkHttpClient client = new OkHttpClient.Builder().followRedirects(false).build();
-
-  /** Notably, javascript makes pre-flight requests, and won't POST spans if disallowed! */
-  @Test public void shouldAllowConfiguredOrigin_preflight() throws Exception {
-    shouldPermitPreflight(optionsForOrigin("GET", "/api/v2/traces", ALLOWED_ORIGIN));
-    shouldPermitPreflight(optionsForOrigin("POST", "/api/v2/spans", ALLOWED_ORIGIN));
-  }
-
-  static void shouldPermitPreflight(Response response) {
-    assertThat(response.isSuccessful())
-      .withFailMessage(response.toString())
-      .isTrue();
-    assertThat(response.header("vary")).contains("origin");
-    assertThat(response.header("access-control-allow-origin")).contains(ALLOWED_ORIGIN);
-    assertThat(response.header("access-control-allow-methods"))
-      .contains(response.request().header("access-control-request-method"));
-    assertThat(response.header("access-control-allow-credentials")).isNull();
-    assertThat(response.header("access-control-allow-headers")).contains("content-type");
-  }
-
-  @Test public void shouldAllowConfiguredOrigin() throws Exception {
-    shouldAllowConfiguredOrigin(getTracesFromOrigin(ALLOWED_ORIGIN));
-    shouldAllowConfiguredOrigin(postSpansFromOrigin(ALLOWED_ORIGIN));
-  }
-
-  static void shouldAllowConfiguredOrigin(Response response) {
-    assertThat(response.header("vary")).contains("origin");
-    assertThat(response.header("access-control-allow-origin"))
-      .contains(response.request().header("origin"));
-    assertThat(response.header("access-control-allow-credentials")).isNull();
-    assertThat(response.header("access-control-allow-headers")).contains("content-type");
-  }
-
-  @Test public void shouldDisallowOrigin() throws Exception {
-    shouldDisallowOrigin(getTracesFromOrigin(DISALLOWED_ORIGIN));
-    shouldDisallowOrigin(postSpansFromOrigin(DISALLOWED_ORIGIN));
-  }
-
-  static void shouldDisallowOrigin(Response response) {
-    assertThat(response.header("vary")).isNull(); // TODO: We used to set vary: origin
-    assertThat(response.header("access-control-allow-credentials")).isNull();
-    assertThat(response.header("access-control-allow-origin")).isNull();
-    assertThat(response.header("access-control-allow-headers")).isNull();
-  }
-
-  private Response optionsForOrigin(String method, String path, String origin) throws IOException {
-    return client.newCall(new Request.Builder()
-      .url(url(server, path))
-      .header("Origin", origin)
-      .header("access-control-request-method", method)
-      .header("access-control-request-headers", "content-type")
-      .method("OPTIONS", null)
-      .build()).execute();
-  }
-
-  private Response getTracesFromOrigin(String origin) throws IOException {
-    return client.newCall(new Request.Builder()
-      .url(url(server, "/api/v2/traces"))
-      .header("Origin", origin)
-      .build()).execute();
-  }
-
-  private Response postSpansFromOrigin(String origin) throws IOException {
-    return client.newCall(new Request.Builder()
-      .url(url(server, "/api/v2/spans"))
-      .header("Origin", origin)
-      .post(RequestBody.create(MediaType.parse("application/json"), "[]"))
-      .build()).execute();
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerHttpCollectorDisabled.java b/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerHttpCollectorDisabled.java
deleted file mode 100644
index 812f0f1..0000000
--- a/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerHttpCollectorDisabled.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.server.internal;
-
-import com.linecorp.armeria.server.Server;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.junit4.SpringRunner;
-import zipkin.server.ZipkinServer;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static zipkin2.server.internal.ITZipkinServer.url;
-
-/**
- * Query-only builds should be able to disable the HTTP collector, so that associated assets 404
- * instead of allowing creation of spans.
- */
-@SpringBootTest(
-  classes = ZipkinServer.class,
-  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
-  properties = {
-    "zipkin.storage.type=", // cheat and test empty storage type
-    "spring.config.name=zipkin-server",
-    "zipkin.collector.http.enabled=false"
-  })
-@RunWith(SpringRunner.class)
-public class ITZipkinServerHttpCollectorDisabled {
-
-  @Autowired Server server;
-  OkHttpClient client = new OkHttpClient.Builder().followRedirects(false).build();
-
-  @Test public void httpCollectorEndpointReturns404() throws Exception {
-    Response response = client.newCall(new Request.Builder()
-      .url(url(server, "/api/v2/spans"))
-      .post(RequestBody.create(null, "[]"))
-      .build()).execute();
-
-    assertThat(response.code()).isEqualTo(404);
-  }
-
-  /** Shows the same http path still works for GET */
-  @Test public void getOnSpansEndpointReturnsOK() throws Exception {
-    Response response = client.newCall(new Request.Builder()
-      .url(url(server, "/api/v2/spans?serviceName=unknown"))
-      .build()).execute();
-
-    assertThat(response.isSuccessful()).isTrue();
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerQueryDisabled.java b/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerQueryDisabled.java
deleted file mode 100644
index 07f288b..0000000
--- a/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerQueryDisabled.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.server.internal;
-
-import com.linecorp.armeria.server.Server;
-import java.io.IOException;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.Response;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.junit4.SpringRunner;
-import zipkin.server.ZipkinServer;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static zipkin2.server.internal.ITZipkinServer.url;
-
-/**
- * Collector-only builds should be able to disable the query (and indirectly the UI), so that
- * associated assets 404 vs throw exceptions.
- */
-@SpringBootTest(
-  classes = ZipkinServer.class,
-  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
-  properties = {
-    "spring.config.name=zipkin-server",
-    "zipkin.query.enabled=false",
-    "zipkin.ui.enabled=false"
-  }
-)
-@RunWith(SpringRunner.class)
-public class ITZipkinServerQueryDisabled {
-  @Autowired Server server;
-  OkHttpClient client = new OkHttpClient.Builder().followRedirects(false).build();
-
-  @Test public void queryRelatedEndpoints404() throws Exception {
-    assertThat(get("/api/v2/traces").code()).isEqualTo(404);
-    assertThat(get("/index.html").code()).isEqualTo(404);
-
-    // but other endpoints are ok
-    assertThat(get("/health").isSuccessful()).isTrue();
-  }
-
-  private Response get(String path) throws IOException {
-    return client.newCall(new Request.Builder().url(url(server, path)).build()).execute();
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerSsl.java b/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerSsl.java
deleted file mode 100644
index d02ce75..0000000
--- a/zipkin-server/src/test/java/zipkin2/server/internal/ITZipkinServerSsl.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.server.internal;
-
-import com.linecorp.armeria.client.ClientFactory;
-import com.linecorp.armeria.client.ClientFactoryBuilder;
-import com.linecorp.armeria.client.HttpClient;
-import com.linecorp.armeria.common.AggregatedHttpMessage;
-import com.linecorp.armeria.common.HttpStatus;
-import com.linecorp.armeria.common.SessionProtocol;
-import com.linecorp.armeria.server.Server;
-import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.junit4.SpringRunner;
-import zipkin.server.ZipkinServer;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * This code ensures you can setup SSL.
- *
- * <p>This is inspired by com.linecorp.armeria.spring.ArmeriaSslConfigurationTest
- */
-@SpringBootTest(
-  classes = ZipkinServer.class,
-  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
-  properties = {
-    "spring.config.name=zipkin-server",
-    "armeria.ssl.enabled=true",
-    "armeria.ports[1].port=0",
-    "armeria.ports[1].protocols[0]=https",
-    // redundant in zipkin-server-shared https://github.com/spring-projects/spring-boot/issues/16394
-    "armeria.ports[0].port=${server.port}",
-    "armeria.ports[0].protocols[0]=http",
-  })
-@RunWith(SpringRunner.class)
-public class ITZipkinServerSsl {
-  @Autowired Server server;
-
-  // We typically use OkHttp in our tests, but Armeria bundles a handy insecure trust manager
-  final ClientFactory clientFactory = new ClientFactoryBuilder()
-    .sslContextCustomizer(b -> b.trustManager(InsecureTrustManagerFactory.INSTANCE))
-    .build();
-
-  @Test public void callHealthEndpoint_HTTP() {
-    callHealthEndpoint(SessionProtocol.HTTP);
-  }
-
-  @Test public void callHealthEndpoint_HTTPS() {
-    callHealthEndpoint(SessionProtocol.HTTPS);
-  }
-
-  void callHealthEndpoint(SessionProtocol http) {
-    AggregatedHttpMessage response = HttpClient.of(clientFactory, baseUrl(server, http))
-      .get("/health")
-      .aggregate().join();
-
-    assertThat(response.status()).isEqualTo(HttpStatus.OK);
-  }
-
-  static String baseUrl(Server server, SessionProtocol protocol) {
-    return server.activePorts().values().stream()
-      .filter(p -> p.hasProtocol(protocol)).findAny()
-      .map(p -> protocol.uriText() + "://127.0.0.1:" + p.localAddress().getPort())
-      .orElseThrow(() -> new AssertionError(protocol + " port not open"));
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/ZipkinServerConfigurationTest.java b/zipkin-server/src/test/java/zipkin2/server/internal/ZipkinServerConfigurationTest.java
deleted file mode 100644
index a194a56..0000000
--- a/zipkin-server/src/test/java/zipkin2/server/internal/ZipkinServerConfigurationTest.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.server.internal;
-
-import brave.Tracing;
-import com.linecorp.armeria.spring.actuate.ArmeriaSpringActuatorAutoConfiguration;
-import io.micrometer.core.instrument.MeterRegistry;
-import io.micrometer.prometheus.PrometheusConfig;
-import io.micrometer.prometheus.PrometheusMeterRegistry;
-import org.junit.After;
-import org.junit.Test;
-import org.springframework.beans.factory.NoSuchBeanDefinitionException;
-import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
-import org.springframework.boot.actuate.health.HealthAggregator;
-import org.springframework.boot.actuate.health.OrderedHealthAggregator;
-import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
-import org.springframework.boot.test.util.TestPropertyValues;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import zipkin2.server.internal.brave.TracingConfiguration;
-import zipkin2.storage.StorageComponent;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;
-
-public class ZipkinServerConfigurationTest {
-  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
-
-  @After public void close() {
-    context.close();
-  }
-
-  @Test public void httpCollector_enabledByDefault() {
-    context.register(
-      ArmeriaSpringActuatorAutoConfiguration.class,
-      EndpointAutoConfiguration.class,
-      PropertyPlaceholderAutoConfiguration.class,
-      ZipkinServerConfigurationTest.Config.class,
-      ZipkinServerConfiguration.class,
-      ZipkinHttpCollector.class
-    );
-    context.refresh();
-
-    assertThat(context.getBean(ZipkinHttpCollector.class)).isNotNull();
-  }
-
-  @Test(expected = NoSuchBeanDefinitionException.class)
-  public void httpCollector_canDisable() {
-    TestPropertyValues.of("zipkin.collector.http.enabled:false").applyTo(context);
-    context.register(
-      ArmeriaSpringActuatorAutoConfiguration.class,
-      EndpointAutoConfiguration.class,
-      PropertyPlaceholderAutoConfiguration.class,
-      ZipkinServerConfigurationTest.Config.class,
-      ZipkinServerConfiguration.class,
-      ZipkinHttpCollector.class
-    );
-    context.refresh();
-
-    context.getBean(ZipkinHttpCollector.class);
-  }
-
-  @Test public void query_enabledByDefault() {
-    context.register(
-      ArmeriaSpringActuatorAutoConfiguration.class,
-      EndpointAutoConfiguration.class,
-      PropertyPlaceholderAutoConfiguration.class,
-      ZipkinServerConfigurationTest.Config.class,
-      ZipkinServerConfiguration.class,
-      ZipkinQueryApiV2.class
-    );
-    context.refresh();
-
-    assertThat(context.getBean(ZipkinQueryApiV2.class)).isNotNull();
-  }
-
-  @Test public void query_canDisable() {
-    TestPropertyValues.of("zipkin.query.enabled:false").applyTo(context);
-    context.register(
-      ArmeriaSpringActuatorAutoConfiguration.class,
-      EndpointAutoConfiguration.class,
-      PropertyPlaceholderAutoConfiguration.class,
-      ZipkinServerConfigurationTest.Config.class,
-      ZipkinServerConfiguration.class,
-      ZipkinQueryApiV2.class
-    );
-    context.refresh();
-
-    try {
-      context.getBean(ZipkinQueryApiV2.class);
-      failBecauseExceptionWasNotThrown(NoSuchBeanDefinitionException.class);
-    } catch (NoSuchBeanDefinitionException e) {
-    }
-  }
-
-  @Test public void selfTracing_canEnable() {
-    TestPropertyValues.of("zipkin.self-tracing.enabled:true").applyTo(context);
-    context.register(
-      ArmeriaSpringActuatorAutoConfiguration.class,
-      EndpointAutoConfiguration.class,
-      PropertyPlaceholderAutoConfiguration.class,
-      ZipkinServerConfigurationTest.Config.class,
-      ZipkinServerConfiguration.class,
-      TracingConfiguration.class
-    );
-    context.refresh();
-
-    context.getBean(Tracing.class).close();
-  }
-
-  @Test public void search_canDisable() {
-    TestPropertyValues.of("zipkin.storage.search-enabled:false").applyTo(context);
-    context.register(
-      ArmeriaSpringActuatorAutoConfiguration.class,
-      EndpointAutoConfiguration.class,
-      PropertyPlaceholderAutoConfiguration.class,
-      ZipkinServerConfigurationTest.Config.class,
-      ZipkinServerConfiguration.class
-    );
-    context.refresh();
-
-    StorageComponent v2Storage = context.getBean(StorageComponent.class);
-    assertThat(v2Storage)
-      .extracting("searchEnabled")
-      .containsExactly(false);
-  }
-
-  @Configuration
-  public static class Config {
-    @Bean
-    public HealthAggregator healthAggregator() {
-      return new OrderedHealthAggregator();
-    }
-
-    @Bean
-    MeterRegistry registry () {
-      return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
-    }
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/brave/ITZipkinSelfTracing.java b/zipkin-server/src/test/java/zipkin2/server/internal/brave/ITZipkinSelfTracing.java
deleted file mode 100644
index 597d9c3..0000000
--- a/zipkin-server/src/test/java/zipkin2/server/internal/brave/ITZipkinSelfTracing.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.server.internal.brave;
-
-import com.linecorp.armeria.server.Server;
-import java.io.IOException;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.junit4.SpringRunner;
-import zipkin.server.ZipkinServer;
-import zipkin2.storage.InMemoryStorage;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static zipkin2.server.internal.ITZipkinServer.url;
-
-@SpringBootTest(
-  classes = ZipkinServer.class,
-  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
-  properties = {
-    "spring.config.name=zipkin-server",
-    "zipkin.self-tracing.enabled=true"
-  })
-@RunWith(SpringRunner.class)
-public class ITZipkinSelfTracing {
-  @Autowired TracingStorageComponent storageComponent;
-  @Autowired Server server;
-
-  OkHttpClient client = new OkHttpClient.Builder().followRedirects(false).build();
-
-  @Before
-  public void clear() {
-    ((InMemoryStorage) storageComponent.delegate).clear();
-  }
-
-  @Test
-  public void getIsTraced_v2() throws Exception {
-    assertThat(get("v2").body().string()).isEqualTo("[]");
-
-    Thread.sleep(1000);
-
-    assertThat(get("v2").body().string()).isEqualTo("[\"zipkin-server\"]");
-  }
-
-  @Test
-  public void postIsTraced_v1() throws Exception {
-    post("v1");
-
-    Thread.sleep(1000);
-
-    assertThat(get("v2").body().string()).isEqualTo("[\"zipkin-server\"]");
-  }
-
-  @Test
-  public void postIsTraced_v2() throws Exception {
-    post("v2");
-
-    Thread.sleep(1000);
-
-    assertThat(get("v2").body().string()).isEqualTo("[\"zipkin-server\"]");
-  }
-
-  private void post(String version) throws IOException {
-    client
-        .newCall(
-            new Request.Builder()
-                .url(url(server,  "/api/" + version + "/spans"))
-                .header("x-b3-sampled", "1") // we don't trace POST by default
-                .post(RequestBody.create(null, "[" + "]"))
-                .build())
-        .execute();
-  }
-
-  private Response get(String version) throws IOException {
-    return client
-        .newCall(new Request.Builder()
-          .url(url(server, "/api/" + version + "/services"))
-          .build())
-        .execute();
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/elasticsearch/BasicAuthInterceptorTest.java b/zipkin-server/src/test/java/zipkin2/server/internal/elasticsearch/BasicAuthInterceptorTest.java
deleted file mode 100644
index 443f7cc..0000000
--- a/zipkin-server/src/test/java/zipkin2/server/internal/elasticsearch/BasicAuthInterceptorTest.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.server.internal.elasticsearch;
-
-import java.io.IOException;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.mockwebserver.MockResponse;
-import okhttp3.mockwebserver.MockWebServer;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-
-public class BasicAuthInterceptorTest {
-
-  @Rule public ExpectedException thrown = ExpectedException.none();
-
-  private MockWebServer mockWebServer;
-  private OkHttpClient client;
-
-  @Before
-  public void beforeEach() {
-    BasicAuthInterceptor interceptor =
-        new BasicAuthInterceptor(new ZipkinElasticsearchStorageProperties());
-    client = new OkHttpClient.Builder().addNetworkInterceptor(interceptor).build();
-    mockWebServer = new MockWebServer();
-  }
-
-  @After
-  public void afterEach() throws IOException {
-    client.dispatcher().executorService().shutdownNow();
-    mockWebServer.close();
-  }
-
-  @Test
-  public void intercept_whenESReturns403AndJsonBody_throwsWithResponseBodyMessage()
-      throws Exception {
-
-    thrown.expect(IllegalStateException.class);
-    thrown.expectMessage("Sadness.");
-
-    mockWebServer.enqueue(
-        new MockResponse().setResponseCode(403).setBody("{\"message\":\"Sadness.\"}"));
-
-    client.newCall(new Request.Builder().url(mockWebServer.url("/")).build()).execute();
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/kafka/ZipkinKafkaCollectorConfigurationTest.java b/zipkin-server/src/test/java/zipkin2/server/internal/kafka/ZipkinKafkaCollectorConfigurationTest.java
deleted file mode 100644
index 6be4bad..0000000
--- a/zipkin-server/src/test/java/zipkin2/server/internal/kafka/ZipkinKafkaCollectorConfigurationTest.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.server.internal.kafka;
-
-import org.junit.After;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.springframework.beans.factory.NoSuchBeanDefinitionException;
-import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
-import org.springframework.boot.test.util.TestPropertyValues;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import zipkin2.collector.CollectorMetrics;
-import zipkin2.collector.CollectorSampler;
-import zipkin2.collector.kafka.KafkaCollector;
-import zipkin2.storage.InMemoryStorage;
-import zipkin2.storage.StorageComponent;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class ZipkinKafkaCollectorConfigurationTest {
-
-  @Rule public ExpectedException thrown = ExpectedException.none();
-
-  AnnotationConfigApplicationContext context;
-
-  @After
-  public void close() {
-    if (context != null) {
-      context.close();
-    }
-  }
-
-  @Test
-  public void doesNotProvideCollectorComponent_whenBootstrapServersUnset() {
-    context = new AnnotationConfigApplicationContext();
-    context.register(
-        PropertyPlaceholderAutoConfiguration.class,
-        ZipkinKafkaCollectorConfiguration.class,
-        InMemoryConfiguration.class);
-    context.refresh();
-
-    thrown.expect(NoSuchBeanDefinitionException.class);
-    context.getBean(KafkaCollector.class);
-  }
-
-  @Test
-  public void providesCollectorComponent_whenBootstrapServersEmptyString() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of("zipkin.collector.kafka.bootstrap-servers:").applyTo(context);
-    context.register(
-        PropertyPlaceholderAutoConfiguration.class,
-        ZipkinKafkaCollectorConfiguration.class,
-        InMemoryConfiguration.class);
-    context.refresh();
-
-    thrown.expect(NoSuchBeanDefinitionException.class);
-    context.getBean(KafkaCollector.class);
-  }
-
-  @Test
-  public void providesCollectorComponent_whenBootstrapServersSet() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of("zipkin.collector.kafka.bootstrap-servers:localhost:9091").applyTo(context);
-    context.register(
-        PropertyPlaceholderAutoConfiguration.class,
-        ZipkinKafkaCollectorConfiguration.class,
-        InMemoryConfiguration.class);
-    context.refresh();
-
-    assertThat(context.getBean(KafkaCollector.class)).isNotNull();
-  }
-
-  @Configuration
-  static class InMemoryConfiguration {
-    @Bean
-    CollectorSampler sampler() {
-      return CollectorSampler.ALWAYS_SAMPLE;
-    }
-
-    @Bean
-    CollectorMetrics metrics() {
-      return CollectorMetrics.NOOP_METRICS;
-    }
-
-    @Bean
-    StorageComponent storage() {
-      return InMemoryStorage.newBuilder().build();
-    }
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/kafka/ZipkinKafkaCollectorPropertiesTest.java b/zipkin-server/src/test/java/zipkin2/server/internal/kafka/ZipkinKafkaCollectorPropertiesTest.java
deleted file mode 100644
index 9afbe6d..0000000
--- a/zipkin-server/src/test/java/zipkin2/server/internal/kafka/ZipkinKafkaCollectorPropertiesTest.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.server.internal.kafka;
-
-import org.junit.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class ZipkinKafkaCollectorPropertiesTest {
-  @Test
-  public void stringPropertiesConvertEmptyStringsToNull() {
-    final ZipkinKafkaCollectorProperties properties = new ZipkinKafkaCollectorProperties();
-    properties.setBootstrapServers("");
-    properties.setGroupId("");
-    properties.setTopic("");
-    assertThat(properties.getBootstrapServers()).isNull();
-    assertThat(properties.getGroupId()).isNull();
-    assertThat(properties.getTopic()).isNull();
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/rabbitmq/ZipkinRabbitMQCollectorConfigurationTest.java b/zipkin-server/src/test/java/zipkin2/server/internal/rabbitmq/ZipkinRabbitMQCollectorConfigurationTest.java
deleted file mode 100644
index 9232137..0000000
--- a/zipkin-server/src/test/java/zipkin2/server/internal/rabbitmq/ZipkinRabbitMQCollectorConfigurationTest.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.server.internal.rabbitmq;
-
-import org.junit.After;
-import org.junit.Ignore;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.springframework.beans.factory.NoSuchBeanDefinitionException;
-import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
-import org.springframework.boot.test.util.TestPropertyValues;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import zipkin2.collector.CollectorMetrics;
-import zipkin2.collector.CollectorSampler;
-import zipkin2.collector.rabbitmq.RabbitMQCollector;
-import zipkin2.storage.InMemoryStorage;
-import zipkin2.storage.StorageComponent;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class ZipkinRabbitMQCollectorConfigurationTest {
-
-  @Rule public ExpectedException thrown = ExpectedException.none();
-
-  private AnnotationConfigApplicationContext context;
-
-  @After
-  public void close() {
-    if (context != null) {
-      context.close();
-    }
-  }
-
-  @Test
-  public void doesNotProvideCollectorComponent_whenAddressAndUriNotSet() {
-    context = new AnnotationConfigApplicationContext();
-    context.register(
-        PropertyPlaceholderAutoConfiguration.class,
-        ZipkinRabbitMQCollectorConfiguration.class,
-        InMemoryConfiguration.class);
-    context.refresh();
-
-    thrown.expect(NoSuchBeanDefinitionException.class);
-    context.getBean(RabbitMQCollector.class);
-  }
-
-  @Test
-  public void doesNotProvideCollectorComponent_whenAddressesAndUriIsEmptyString() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of(
-      "zipkin.collector.rabbitmq.addresses:",
-      "zipkin.collector.rabbitmq.uri:")
-    .applyTo(context);
-    context.register(
-        PropertyPlaceholderAutoConfiguration.class,
-        ZipkinRabbitMQCollectorConfiguration.class,
-        InMemoryConfiguration.class);
-    context.refresh();
-
-    thrown.expect(NoSuchBeanDefinitionException.class);
-    context.getBean(RabbitMQCollector.class);
-  }
-
-  @Test
-  @Ignore
-  public void providesCollectorComponent_whenAddressesSet() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of("zipkin.collector.rabbitmq.addresses=localhost:5672").applyTo(context);
-    context.register(
-        PropertyPlaceholderAutoConfiguration.class,
-        ZipkinRabbitMQCollectorConfiguration.class,
-        InMemoryConfiguration.class);
-    context.refresh();
-
-    assertThat(context.getBean(RabbitMQCollector.class)).isNotNull();
-  }
-
-  @Configuration
-  static class InMemoryConfiguration {
-    @Bean
-    CollectorSampler sampler() {
-      return CollectorSampler.ALWAYS_SAMPLE;
-    }
-
-    @Bean
-    CollectorMetrics metrics() {
-      return CollectorMetrics.NOOP_METRICS;
-    }
-
-    @Bean
-    StorageComponent storage() {
-      return InMemoryStorage.newBuilder().build();
-    }
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/rabbitmq/ZipkinRabbitMQCollectorPropertiesTest.java b/zipkin-server/src/test/java/zipkin2/server/internal/rabbitmq/ZipkinRabbitMQCollectorPropertiesTest.java
deleted file mode 100644
index e536478..0000000
--- a/zipkin-server/src/test/java/zipkin2/server/internal/rabbitmq/ZipkinRabbitMQCollectorPropertiesTest.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.server.internal.rabbitmq;
-
-import com.rabbitmq.client.ConnectionFactory;
-import java.net.URI;
-import java.util.Collections;
-import org.junit.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class ZipkinRabbitMQCollectorPropertiesTest {
-  ZipkinRabbitMQCollectorProperties properties = new ZipkinRabbitMQCollectorProperties();
-
-  @Test
-  public void uriProperlyParsedAndIgnoresOtherProperties_whenUriSet() throws Exception {
-    properties.setUri(URI.create("amqp://admin:admin@localhost:5678/myv"));
-    properties.setAddresses(Collections.singletonList("will_not^work!"));
-    properties.setUsername("bob");
-    properties.setPassword("letmein");
-    properties.setVirtualHost("drwho");
-
-    assertThat(properties.toBuilder())
-        .extracting("connectionFactory")
-        .allSatisfy(
-            object -> {
-              ConnectionFactory connFactory = (ConnectionFactory) object;
-              assertThat(connFactory.getHost()).isEqualTo("localhost");
-              assertThat(connFactory.getPort()).isEqualTo(5678);
-              assertThat(connFactory.getUsername()).isEqualTo("admin");
-              assertThat(connFactory.getPassword()).isEqualTo("admin");
-              assertThat(connFactory.getVirtualHost()).isEqualTo("myv");
-            });
-  }
-
-  /** This prevents an empty RABBIT_URI variable from being mistaken as a real one */
-  @Test
-  public void ignoresEmptyURI() {
-    properties.setUri(URI.create(""));
-
-    assertThat(properties.getUri()).isNull();
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/ui/ITZipkinUiConfiguration.java b/zipkin-server/src/test/java/zipkin2/server/internal/ui/ITZipkinUiConfiguration.java
deleted file mode 100644
index 444eb3a..0000000
--- a/zipkin-server/src/test/java/zipkin2/server/internal/ui/ITZipkinUiConfiguration.java
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.server.internal.ui;
-
-import com.linecorp.armeria.client.HttpClient;
-import com.linecorp.armeria.common.AggregatedHttpMessage;
-import com.linecorp.armeria.common.HttpHeaderNames;
-import com.linecorp.armeria.common.HttpHeaders;
-import com.linecorp.armeria.common.HttpMethod;
-import com.linecorp.armeria.server.Server;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UncheckedIOException;
-import java.net.URL;
-import java.util.stream.Stream;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.Response;
-import okio.Okio;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.context.annotation.Import;
-import org.springframework.test.context.junit4.SpringRunner;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-@RunWith(SpringRunner.class)
-@SpringBootTest(
-  classes = ITZipkinUiConfiguration.TestServer.class,
-  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
-  properties = {
-    "zipkin.ui.base-path=/foozipkin",
-    "server.compression.enabled=true",
-    "server.compression.min-response-size=128"
-  }
-)
-public class ITZipkinUiConfiguration {
-
-  @Autowired Server server;
-  OkHttpClient client = new OkHttpClient.Builder().followRedirects(false).build();
-
-  /** The zipkin-ui is a single-page app. This prevents reloading all resources on each click. */
-  @Test public void setsMaxAgeOnUiResources() throws Exception {
-    assertThat(get("/zipkin/config.json").header("Cache-Control"))
-      .isEqualTo("max-age=600");
-    assertThat(get("/zipkin/index.html").header("Cache-Control"))
-      .isEqualTo("max-age=60");
-    assertThat(get("/zipkin/test.txt").header("Cache-Control"))
-      .isEqualTo("max-age=31536000");
-  }
-
-  @Test public void redirectsIndex() throws Exception {
-    String index = get("/zipkin/index.html").body().string();
-
-    client = new OkHttpClient.Builder().followRedirects(true).build();
-
-    Stream.of("/zipkin", "/").forEach(path -> {
-      try {
-        assertThat(get(path).body().string()).isEqualTo(index);
-      } catch (IOException e) {
-        throw new UncheckedIOException(e);
-      }
-    });
-  }
-
-  /** Browsers honor conditional requests such as eTag. Let's make sure the server does */
-  @Test public void conditionalRequests() throws Exception {
-    Stream.of("/zipkin/config.json", "/zipkin/index.html", "/zipkin/test.txt").forEach(path -> {
-      try {
-        String etag = get(path).header("etag");
-        assertThat(conditionalGet(path, etag).code())
-          .isEqualTo(304);
-        assertThat(conditionalGet(path, "aargh").code())
-          .isEqualTo(200);
-      } catch (IOException e) {
-        throw new UncheckedIOException(e);
-      }
-    });
-  }
-
-  /** Some assets are pretty big. ensure they use compression. */
-  @Test public void supportsCompression() throws Exception {
-    assertThat(getContentEncodingFromRequestThatAcceptsGzip("/zipkin/test.txt"))
-      .isNull(); // too small to compress
-    assertThat(getContentEncodingFromRequestThatAcceptsGzip("/zipkin/config.json"))
-      .isEqualTo("gzip");
-  }
-
-  /**
-   * The test sets the property {@code zipkin.ui.base-path=/foozipkin}, which should reflect in
-   * index.html
-   */
-  @Test public void replacesBaseTag() throws Exception {
-    assertThat(get("/zipkin/index.html").body().string())
-      .isEqualToIgnoringWhitespace(stringFromClasspath("zipkin-ui/index.html")
-        .replace("<base href=\"/\" />", "<base href=\"/foozipkin/\">"));
-  }
-
-  /** index.html is served separately. This tests other content is also loaded from the classpath. */
-  @Test public void servesOtherContentFromClasspath() throws Exception {
-    assertThat(get("/zipkin/test.txt").body().string())
-      .isEqualToIgnoringWhitespace(stringFromClasspath("zipkin-ui/test.txt"));
-  }
-
-  @EnableAutoConfiguration
-  @Import(ZipkinUiConfiguration.class)
-  public static class TestServer {
-  }
-
-  private String stringFromClasspath(String path) throws IOException {
-    URL url = getClass().getClassLoader().getResource(path);
-    assertThat(url).isNotNull();
-
-    try (InputStream fromClasspath = url.openStream()) {
-      return Okio.buffer(Okio.source(fromClasspath)).readUtf8();
-    }
-  }
-
-  private Response get(String path) throws IOException {
-    return client.newCall(new Request.Builder()
-      .url("http://localhost:" + port() + path)
-      .build()).execute();
-  }
-
-  private Response conditionalGet(String path, String etag) throws IOException {
-    return client.newCall(new Request.Builder()
-      .url("http://localhost:" + port() + path)
-      .header("If-None-Match", etag)
-      .build()).execute();
-  }
-
-  private String getContentEncodingFromRequestThatAcceptsGzip(String path) {
-    // We typically use OkHttp in our tests, but that automatically unzips..
-    AggregatedHttpMessage response = HttpClient.of("http://localhost:" + port())
-        .execute(HttpHeaders.of(HttpMethod.GET, path).set(HttpHeaderNames.ACCEPT_ENCODING, "gzip"))
-        .aggregate().join();
-    return response.headers().get(HttpHeaderNames.CONTENT_ENCODING);
-  }
-
-  private int port() {
-    return server.activePort().get().localAddress().getPort();
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/ui/ZipkinUiConfigurationTest.java b/zipkin-server/src/test/java/zipkin2/server/internal/ui/ZipkinUiConfigurationTest.java
deleted file mode 100644
index 01d332d..0000000
--- a/zipkin-server/src/test/java/zipkin2/server/internal/ui/ZipkinUiConfigurationTest.java
+++ /dev/null
@@ -1,214 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.server.internal.ui;
-
-import com.linecorp.armeria.common.AggregatedHttpMessage;
-import com.linecorp.armeria.common.HttpHeaderNames;
-import com.linecorp.armeria.common.HttpHeaders;
-import com.linecorp.armeria.common.HttpMethod;
-import com.linecorp.armeria.common.HttpRequest;
-import com.linecorp.armeria.common.MediaType;
-import com.linecorp.armeria.server.ServiceRequestContext;
-import io.netty.handler.codec.http.cookie.ClientCookieEncoder;
-import io.netty.handler.codec.http.cookie.Cookie;
-import io.netty.handler.codec.http.cookie.DefaultCookie;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import org.junit.After;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.springframework.beans.factory.BeanCreationException;
-import org.springframework.beans.factory.NoSuchBeanDefinitionException;
-import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
-import org.springframework.boot.test.util.TestPropertyValues;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
-import org.springframework.core.io.ClassPathResource;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.hamcrest.CoreMatchers.isA;
-import static org.junit.internal.matchers.ThrowableCauseMatcher.hasCause;
-
-public class ZipkinUiConfigurationTest {
-
-  AnnotationConfigApplicationContext context;
-
-  @After
-  public void close() {
-    if (context != null) {
-      context.close();
-    }
-  }
-
-  @Rule
-  public ExpectedException thrown = ExpectedException.none();
-
-  @Test
-  public void indexHtmlFromClasspath() {
-    context = createContext();
-
-    assertThat(context.getBean(ZipkinUiConfiguration.class).indexHtml)
-      .isNotNull();
-  }
-
-  @Test
-  public void indexContentType() {
-    context = createContext();
-    assertThat(
-      serveIndex().headers().contentType())
-      .isEqualTo(MediaType.HTML_UTF_8);
-  }
-
-  @Test
-  public void invalidIndexHtml() {
-    // I failed to make Jsoup barf, even on nonsense like: "<head wait no I changed my mind this HTML is totally invalid <<<<<<<<<<<"
-    // So let's just run with a case where the file doesn't exist
-    context = createContextWithOverridenProperty("zipkin.ui.basepath:/foo/bar");
-    ZipkinUiConfiguration ui = context.getBean(ZipkinUiConfiguration.class);
-    ui.indexHtml = new ClassPathResource("does-not-exist.html");
-
-    thrown.expect(RuntimeException.class);
-    // There's a BeanInstantiationException nested in between BeanCreationException and IOException,
-    // so we go one level deeper about causes. There's no `expectRootCause`.
-    thrown.expectCause(hasCause(hasCause(isA(BeanCreationException.class))));
-    serveIndex();
-  }
-
-  @Test
-  public void canOverridesProperty_defaultLookback() {
-    context = createContextWithOverridenProperty("zipkin.ui.defaultLookback:100");
-
-    assertThat(context.getBean(ZipkinUiProperties.class).getDefaultLookback())
-      .isEqualTo(100);
-  }
-
-  @Test
-  public void canOverrideProperty_logsUrl() {
-    final String url = "http://mycompany.com/kibana";
-    context = createContextWithOverridenProperty("zipkin.ui.logs-url:" + url);
-
-    assertThat(context.getBean(ZipkinUiProperties.class).getLogsUrl()).isEqualTo(url);
-  }
-
-  @Test
-  public void logsUrlIsNullIfOverridenByEmpty() {
-    context = createContextWithOverridenProperty("zipkin.ui.logs-url:");
-
-    assertThat(context.getBean(ZipkinUiProperties.class).getLogsUrl()).isNull();
-  }
-
-  @Test
-  public void logsUrlIsNullByDefault() {
-    context = createContext();
-
-    assertThat(context.getBean(ZipkinUiProperties.class).getLogsUrl()).isNull();
-  }
-
-  @Test(expected = NoSuchBeanDefinitionException.class)
-  public void canOverridesProperty_disable() {
-    context = createContextWithOverridenProperty("zipkin.ui.enabled:false");
-
-    context.getBean(ZipkinUiProperties.class);
-  }
-
-  @Test
-  public void canOverridesProperty_searchEnabled() {
-    context = createContextWithOverridenProperty("zipkin.ui.search-enabled:false");
-
-    assertThat(context.getBean(ZipkinUiProperties.class).isSearchEnabled()).isFalse();
-  }
-
-  @Test
-  public void canOverrideProperty_dependencyLowErrorRate() {
-    context = createContextWithOverridenProperty("zipkin.ui.dependency.low-error-rate:0.1");
-
-    assertThat(context.getBean(ZipkinUiProperties.class).getDependency().getLowErrorRate())
-      .isEqualTo(0.1f);
-  }
-
-  @Test
-  public void canOverrideProperty_dependencyHighErrorRate() {
-    context = createContextWithOverridenProperty("zipkin.ui.dependency.high-error-rate:0.1");
-
-    assertThat(context.getBean(ZipkinUiProperties.class).getDependency().getHighErrorRate())
-      .isEqualTo(0.1f);
-  }
-
-  @Test
-  public void defaultBaseUrl_doesNotChangeResource() throws IOException {
-    context = createContext();
-
-    assertThat(new ByteArrayInputStream(serveIndex().content().array()))
-      .hasSameContentAs(getClass().getResourceAsStream("/zipkin-ui/index.html"));
-  }
-
-  @Test
-  public void canOverideProperty_basePath() {
-    context = createContextWithOverridenProperty("zipkin.ui.basepath:/foo/bar");
-
-    assertThat(serveIndex().contentUtf8())
-      .contains("<base href=\"/foo/bar/\">");
-  }
-
-  @Test
-  public void lensCookieOverridesIndex() {
-    context = createContext();
-
-    assertThat(serveIndex(new DefaultCookie("lens", "true")).contentUtf8())
-      .contains("zipkin-lens");
-  }
-
-  @Test
-  public void canOverideProperty_specialCaseRoot() {
-    context = createContextWithOverridenProperty("zipkin.ui.basepath:/");
-
-    assertThat(serveIndex().contentUtf8())
-      .contains("<base href=\"/\">");
-  }
-
-  private AggregatedHttpMessage serveIndex(Cookie... cookies) {
-    HttpHeaders headers = HttpHeaders.of(HttpMethod.GET, "/");
-    String encodedCookies = ClientCookieEncoder.LAX.encode(cookies);
-    if (encodedCookies != null) {
-      headers.set(HttpHeaderNames.COOKIE, encodedCookies);
-    }
-    HttpRequest req = HttpRequest.of(headers);
-    try {
-      return context.getBean(ZipkinUiConfiguration.class).indexSwitchingService()
-        .serve(ServiceRequestContext.of(req), req).aggregate()
-        .get();
-    } catch (Exception e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  private static AnnotationConfigApplicationContext createContext() {
-    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
-    context.register(PropertyPlaceholderAutoConfiguration.class, ZipkinUiConfiguration.class);
-    context.refresh();
-    return context;
-  }
-
-  private static AnnotationConfigApplicationContext createContextWithOverridenProperty(
-    String pair) {
-    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of(pair).applyTo(context);
-    context.register(PropertyPlaceholderAutoConfiguration.class, ZipkinUiConfiguration.class);
-    context.refresh();
-    return context;
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/storage/cassandra/ZipkinCassandraStorageAutoConfigurationTest.java b/zipkin-server/src/test/java/zipkin2/storage/cassandra/ZipkinCassandraStorageAutoConfigurationTest.java
deleted file mode 100644
index 8c391cb..0000000
--- a/zipkin-server/src/test/java/zipkin2/storage/cassandra/ZipkinCassandraStorageAutoConfigurationTest.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.storage.cassandra;
-
-import org.junit.After;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.springframework.beans.factory.NoSuchBeanDefinitionException;
-import org.springframework.boot.test.util.TestPropertyValues;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
-import zipkin2.server.internal.cassandra3.Access;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class ZipkinCassandraStorageAutoConfigurationTest {
-
-  @Rule public ExpectedException thrown = ExpectedException.none();
-
-  AnnotationConfigApplicationContext context;
-
-  @After
-  public void close() {
-    if (context != null) {
-      context.close();
-    }
-  }
-
-  @Test
-  public void doesntProvidesStorageComponent_whenStorageTypeNotCassandra() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of("zipkin.storage.type:elasticsearch").applyTo(context);
-    Access.registerCassandra3(context);
-    context.refresh();
-
-    thrown.expect(NoSuchBeanDefinitionException.class);
-    context.getBean(CassandraStorage.class);
-  }
-
-  @Test
-  public void providesStorageComponent_whenStorageTypeCassandra() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of("zipkin.storage.type:cassandra3").applyTo(context);
-    Access.registerCassandra3(context);
-    context.refresh();
-
-    assertThat(context.getBean(CassandraStorage.class)).isNotNull();
-  }
-
-  @Test
-  public void canOverridesProperty_contactPoints() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of(
-        "zipkin.storage.type:cassandra3",
-        "zipkin.storage.cassandra3.contact-points:host1,host2" // note snake-case supported
-        ).applyTo(context);
-    Access.registerCassandra3(context);
-    context.refresh();
-
-    assertThat(context.getBean(CassandraStorage.class).contactPoints()).isEqualTo("host1,host2");
-  }
-
-  @Test
-  public void strictTraceId_defaultsToTrue() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of("zipkin.storage.type:cassandra3").applyTo(context);
-    Access.registerCassandra3(context);
-    context.refresh();
-
-    assertThat(context.getBean(CassandraStorage.class).strictTraceId()).isTrue();
-  }
-
-  @Test
-  public void strictTraceId_canSetToFalse() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of(
-        "zipkin.storage.type:cassandra3",
-        "zipkin.storage.strict-trace-id:false")
-    .applyTo(context);
-    Access.registerCassandra3(context);
-    context.refresh();
-
-    assertThat(context.getBean(CassandraStorage.class).strictTraceId()).isFalse();
-  }
-
-  @Test
-  public void searchEnabled_canSetToFalse() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of(
-        "zipkin.storage.type:cassandra3",
-        "zipkin.storage.search-enabled:false")
-    .applyTo(context);
-    Access.registerCassandra3(context);
-    context.refresh();
-
-    assertThat(context.getBean(CassandraStorage.class).searchEnabled()).isFalse();
-  }
-
-  @Test
-  public void autocompleteKeys_list() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of(
-      "zipkin.storage.type:cassandra3",
-      "zipkin.storage.autocomplete-keys:environment")
-      .applyTo(context);
-    Access.registerCassandra3(context);
-    context.refresh();
-
-    assertThat(context.getBean(CassandraStorage.class).autocompleteKeys())
-      .containsOnly("environment");
-  }
-
-  @Test
-  public void autocompleteTtl() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of(
-      "zipkin.storage.type:cassandra3",
-      "zipkin.storage.autocomplete-ttl:60000")
-      .applyTo(context);
-    Access.registerCassandra3(context);
-    context.refresh();
-
-    assertThat(context.getBean(CassandraStorage.class).autocompleteTtl())
-      .isEqualTo(60000);
-  }
-
-  @Test
-  public void autocompleteCardinality() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of(
-      "zipkin.storage.type:cassandra3",
-      "zipkin.storage.autocomplete-cardinality:5000")
-      .applyTo(context);
-    Access.registerCassandra3(context);
-    context.refresh();
-
-    assertThat(context.getBean(CassandraStorage.class).autocompleteCardinality())
-      .isEqualTo(5000);
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/storage/cassandra/v1/ZipkinCassandraStorageConfigurationTest.java b/zipkin-server/src/test/java/zipkin2/storage/cassandra/v1/ZipkinCassandraStorageConfigurationTest.java
deleted file mode 100644
index d281ba4..0000000
--- a/zipkin-server/src/test/java/zipkin2/storage/cassandra/v1/ZipkinCassandraStorageConfigurationTest.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.storage.cassandra.v1;
-
-import org.junit.After;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.springframework.beans.factory.NoSuchBeanDefinitionException;
-import org.springframework.boot.test.util.TestPropertyValues;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
-import zipkin2.server.internal.cassandra.Access;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class ZipkinCassandraStorageConfigurationTest {
-
-  @Rule public ExpectedException thrown = ExpectedException.none();
-
-  AnnotationConfigApplicationContext context;
-
-  @After
-  public void close() {
-    if (context != null) {
-      context.close();
-    }
-  }
-
-  @Test
-  public void doesntProvidesStorageComponent_whenStorageTypeNotCassandra() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of("zipkin.storage.type:elasticsearch").applyTo(context);
-    Access.registerCassandra(context);
-    context.refresh();
-
-    thrown.expect(NoSuchBeanDefinitionException.class);
-    context.getBean(CassandraStorage.class);
-  }
-
-  @Test
-  public void providesStorageComponent_whenStorageTypeCassandra() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of("zipkin.storage.type:cassandra").applyTo(context);
-    Access.registerCassandra(context);
-    context.refresh();
-
-    assertThat(context.getBean(CassandraStorage.class)).isNotNull();
-  }
-
-  @Test
-  public void canOverridesProperty_contactPoints() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of(
-        "zipkin.storage.type:cassandra",
-        "zipkin.storage.cassandra.contact-points:host1,host2" // note snake-case supported
-        ).applyTo(context);
-    Access.registerCassandra(context);
-    context.refresh();
-
-    assertThat(context.getBean(CassandraStorage.class).contactPoints).isEqualTo("host1,host2");
-  }
-
-  @Test
-  public void strictTraceId_defaultsToTrue() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of("zipkin.storage.type:cassandra").applyTo(context);
-    Access.registerCassandra(context);
-    context.refresh();
-    assertThat(context.getBean(CassandraStorage.class).strictTraceId).isTrue();
-  }
-
-  @Test
-  public void strictTraceId_canSetToFalse() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of(
-      "zipkin.storage.type:cassandra",
-      "zipkin.storage.strict-trace-id:false")
-    .applyTo(context);
-    Access.registerCassandra(context);
-    context.refresh();
-
-    assertThat(context.getBean(CassandraStorage.class).strictTraceId).isFalse();
-  }
-
-  @Test
-  public void autocompleteKeys_list() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of(
-      "zipkin.storage.type:cassandra",
-      "zipkin.storage.autocomplete-keys:environment")
-      .applyTo(context);
-    Access.registerCassandra(context);
-    context.refresh();
-
-    assertThat(context.getBean(CassandraStorage.class).autocompleteKeys)
-      .containsOnly("environment");
-  }
-
-  @Test
-  public void autocompleteTtl() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of(
-      "zipkin.storage.type:cassandra",
-      "zipkin.storage.autocomplete-ttl:60000")
-      .applyTo(context);
-    Access.registerCassandra(context);
-    context.refresh();
-
-    assertThat(context.getBean(CassandraStorage.class).autocompleteTtl)
-      .isEqualTo(60000);
-  }
-
-  @Test
-  public void autocompleteCardinality() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of(
-      "zipkin.storage.type:cassandra",
-      "zipkin.storage.autocomplete-cardinality:5000")
-      .applyTo(context);
-    Access.registerCassandra(context);
-    context.refresh();
-
-    assertThat(context.getBean(CassandraStorage.class).autocompleteCardinality)
-      .isEqualTo(5000);
-  }
-}
diff --git a/zipkin-server/src/test/java/zipkin2/storage/mysql/v1/ZipkinMySQLStorageConfigurationTest.java b/zipkin-server/src/test/java/zipkin2/storage/mysql/v1/ZipkinMySQLStorageConfigurationTest.java
deleted file mode 100644
index 9b90ef6..0000000
--- a/zipkin-server/src/test/java/zipkin2/storage/mysql/v1/ZipkinMySQLStorageConfigurationTest.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 zipkin2.storage.mysql.v1;
-
-import com.zaxxer.hikari.HikariDataSource;
-import org.junit.After;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.springframework.beans.factory.NoSuchBeanDefinitionException;
-import org.springframework.boot.test.util.TestPropertyValues;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
-import zipkin2.server.internal.mysql.Access;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class ZipkinMySQLStorageConfigurationTest {
-
-  @Rule public ExpectedException thrown = ExpectedException.none();
-
-  AnnotationConfigApplicationContext context;
-
-  @After
-  public void close() {
-    if (context != null) {
-      context.close();
-    }
-  }
-
-  @Test
-  public void doesntProvidesStorageComponent_whenStorageTypeNotMySQL() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of("zipkin.storage.type:cassandra").applyTo(context);
-    Access.registerMySQL(context);
-    context.refresh();
-
-    thrown.expect(NoSuchBeanDefinitionException.class);
-    context.getBean(MySQLStorage.class);
-  }
-
-  @Test
-  public void providesStorageComponent_whenStorageTypeMySQL() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of("zipkin.storage.type:mysql").applyTo(context);
-    Access.registerMySQL(context);
-    context.refresh();
-
-    assertThat(context.getBean(MySQLStorage.class)).isNotNull();
-  }
-
-  @Test
-  public void canOverridesProperty_username() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of(
-        "zipkin.storage.type:mysql",
-        "zipkin.storage.mysql.username:robot")
-    .applyTo(context);
-    Access.registerMySQL(context);
-    context.refresh();
-
-    assertThat(context.getBean(HikariDataSource.class).getUsername()).isEqualTo("robot");
-  }
-
-  @Test
-  public void strictTraceId_defaultsToTrue() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of("zipkin.storage.type:mysql").applyTo(context);
-    Access.registerMySQL(context);
-    context.refresh();
-    assertThat(context.getBean(MySQLStorage.class).strictTraceId).isTrue();
-  }
-
-  @Test
-  public void strictTraceId_canSetToFalse() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of(
-        "zipkin.storage.type:mysql",
-        "zipkin.storage.strict-trace-id:false")
-      .applyTo(context);
-    Access.registerMySQL(context);
-    context.refresh();
-
-    assertThat(context.getBean(MySQLStorage.class).strictTraceId).isFalse();
-  }
-
-  @Test
-  public void searchEnabled_canSetToFalse() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of(
-      "zipkin.storage.type:mysql",
-      "zipkin.storage.search-enabled:false")
-      .applyTo(context);
-    Access.registerMySQL(context);
-    context.refresh();
-
-    assertThat(context.getBean(MySQLStorage.class).searchEnabled).isFalse();
-  }
-
-  @Test
-  public void autocompleteKeys_list() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of(
-      "zipkin.storage.type:mysql",
-      "zipkin.storage.autocomplete-keys:environment")
-      .applyTo(context);
-    Access.registerMySQL(context);
-    context.refresh();
-
-    assertThat(context.getBean(MySQLStorage.class).autocompleteKeys)
-      .containsOnly("environment");
-  }
-
-  @Test
-  public void usesJdbcUrl_whenPresent() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of(
-        "zipkin.storage.type:mysql",
-        "zipkin.storage.mysql"
-      + ".jdbc-url:jdbc:mysql://host1,host2,host3/zipkin")
-    .applyTo(context);
-    Access.registerMySQL(context);
-    context.refresh();
-
-    assertThat(context.getBean(HikariDataSource.class).getJdbcUrl()).isEqualTo("jdbc:mysql://host1,host2,host3/zipkin");
-  }
-
-  @Test
-  public void usesRegularConfig_whenBlank() {
-    context = new AnnotationConfigApplicationContext();
-    TestPropertyValues.of(
-        "zipkin.storage.type:mysql",
-        "zipkin.storage.mysql.jdbc-url:",
-        "zipkin.storage.mysql.host:host",
-        "zipkin.storage.mysql.port:3306",
-        "zipkin.storage.mysql.username:root",
-        "zipkin.storage.mysql.password:secret",
-        "zipkin.storage.mysql.db:zipkin")
-    .applyTo(context);
-    Access.registerMySQL(context);
-    context.refresh();
-
-    assertThat(context.getBean(HikariDataSource.class).getJdbcUrl()).isEqualTo("jdbc:mysql://host:3306/zipkin?autoReconnect=true&useSSL=false&useUnicode=yes&characterEncoding=UTF-8");
-  }
-}
diff --git a/zipkin-server/src/test/kotlin/zipkin/server/ITEnableZipkinServer.kt b/zipkin-server/src/test/kotlin/zipkin/server/ITEnableZipkinServer.kt
new file mode 100644
index 0000000..7a383e0
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin/server/ITEnableZipkinServer.kt
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin.server
+
+import com.linecorp.armeria.server.Server
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.test.context.junit4.SpringRunner
+import zipkin2.server.internal.Http
+
+@SpringBootTest(
+  classes = [ITEnableZipkinServer.CustomServer::class],
+  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+  properties = ["spring.config.name=zipkin-server"]
+)
+@RunWith(SpringRunner::class)
+class ITEnableZipkinServer {
+  @SpringBootApplication
+  @EnableZipkinServer
+  open class CustomServer
+
+  @Autowired lateinit var server: Server
+
+  @Test fun writeSpans_noContentTypeIsJson() {
+    assertThat(Http.get(server, "/api/v2/services").code())
+      .isEqualTo(200)
+  }
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/collector/kafka/ZipkinKafkaCollectorPropertiesOverrideTest.kt b/zipkin-server/src/test/kotlin/zipkin2/collector/kafka/ZipkinKafkaCollectorPropertiesOverrideTest.kt
new file mode 100644
index 0000000..32d75b9
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/collector/kafka/ZipkinKafkaCollectorPropertiesOverrideTest.kt
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.collector.kafka
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.springframework.boot.test.util.TestPropertyValues
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
+import zipkin2.server.internal.kafka.Access
+
+@RunWith(Parameterized::class)
+class ZipkinKafkaCollectorPropertiesOverrideTest(
+  val property: String,
+  val value: Any,
+  val builderExtractor: (KafkaCollector.Builder) -> Any
+) {
+
+  companion object {
+    @JvmStatic @Parameterized.Parameters fun data(): List<Array<Any?>> {
+      return listOf(
+        parameters("bootstrap-servers", "127.0.0.1:9092",
+          { b -> b.properties.getProperty("bootstrap.servers") }),
+        parameters("group-id", "zapkin",
+          { b -> b.properties.getProperty("group.id") }),
+        parameters("topic", "zapkin",
+          { b -> b.topic }),
+        parameters("streams", 2,
+          { b -> b.streams }),
+        parameters("overrides.auto.offset.reset", "latest",
+          { b -> b.properties.getProperty("auto.offset.reset") })
+      )
+    }
+
+    /** to allow us to define with a lambda  */
+    internal fun <T> parameters(
+      propertySuffix: String, value: T, builderExtractor: (KafkaCollector.Builder) -> T
+    ): Array<Any?> = arrayOf(propertySuffix, value, builderExtractor)
+  }
+
+  var context = AnnotationConfigApplicationContext()
+
+  @After fun close() {
+    context.close()
+  }
+
+  @Test fun propertyTransferredToCollectorBuilder() {
+    TestPropertyValues.of("zipkin.collector.kafka.$property:$value").applyTo(context)
+    Access.registerKafkaProperties(context)
+    context.refresh()
+
+    assertThat(Access.collectorBuilder(context))
+      .extracting(builderExtractor)
+      .isEqualTo(value)
+  }
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/collector/rabbitmq/ZipkinRabbitMQCollectorPropertiesOverrideTest.kt b/zipkin-server/src/test/kotlin/zipkin2/collector/rabbitmq/ZipkinRabbitMQCollectorPropertiesOverrideTest.kt
new file mode 100644
index 0000000..bce32c2
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/collector/rabbitmq/ZipkinRabbitMQCollectorPropertiesOverrideTest.kt
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.collector.rabbitmq
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.springframework.boot.test.util.TestPropertyValues
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
+import zipkin2.server.internal.rabbitmq.Access
+import java.net.URI
+
+@RunWith(Parameterized::class)
+class ZipkinRabbitMQPropertiesOverrideTest(
+  val property: String,
+  val value: Any,
+  val builderExtractor: (RabbitMQCollector.Builder) -> Any
+) {
+
+  companion object {
+    @JvmStatic @Parameterized.Parameters fun data(): List<Array<Any?>> {
+      return listOf(
+        // intentionally punting on comma-separated form of a list of addresses as it doesn't fit
+        // this unit test. Better to make a separate one than force-fit!
+        parameters("addresses", "localhost:5671",
+          { builder -> builder.addresses[0].toString() }),
+        parameters("concurrency", 2,
+          { builder -> builder.concurrency }),
+        parameters("connectionTimeout", 30000,
+          { builder -> builder.connectionFactory.connectionTimeout }),
+        parameters("password", "admin",
+          { builder -> builder.connectionFactory.password }),
+        parameters("queue", "zapkin",
+          { builder -> builder.queue }),
+        parameters("username", "admin",
+          { builder -> builder.connectionFactory.username }),
+        parameters("virtualHost", "/hello",
+          { builder -> builder.connectionFactory.virtualHost }),
+        parameters("useSsl", true,
+          { builder -> builder.connectionFactory.isSSL }),
+        parameters("uri", URI.create("amqp://localhost"),
+          { builder -> URI.create("amqp://" + builder.connectionFactory.host) })
+      )
+    }
+
+    /** to allow us to define with a lambda  */
+    internal fun <T> parameters(
+      propertySuffix: String, value: T, builderExtractor: (RabbitMQCollector.Builder) -> T
+    ): Array<Any?> = arrayOf(propertySuffix, value, builderExtractor)
+  }
+
+  var context = AnnotationConfigApplicationContext()
+
+  @After fun close() {
+    context.close()
+  }
+
+  @Test fun propertyTransferredToCollectorBuilder() {
+    TestPropertyValues.of("zipkin.collector.rabbitmq.$property:$value").applyTo(context)
+    Access.registerRabbitMQProperties(context)
+    context.refresh()
+
+    assertThat(Access.collectorBuilder(context))
+      .extracting(builderExtractor)
+      .isEqualTo(value)
+  }
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/elasticsearch/ZipkinElasticsearchStorageAutoConfigurationTest.kt b/zipkin-server/src/test/kotlin/zipkin2/elasticsearch/ZipkinElasticsearchStorageAutoConfigurationTest.kt
new file mode 100644
index 0000000..42f2ea9
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/elasticsearch/ZipkinElasticsearchStorageAutoConfigurationTest.kt
@@ -0,0 +1,360 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.elasticsearch
+
+import okhttp3.Interceptor
+import okhttp3.OkHttpClient
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.After
+import org.junit.Test
+import org.springframework.beans.factory.BeanCreationException
+import org.springframework.beans.factory.NoSuchBeanDefinitionException
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.boot.test.util.TestPropertyValues
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import zipkin2.server.internal.elasticsearch.Access
+import java.util.concurrent.TimeUnit
+
+class ZipkinElasticsearchStorageAutoConfigurationTest {
+  val context = AnnotationConfigApplicationContext()
+  @After fun closeContext() = context.close()
+  internal fun es() = context.getBean(ElasticsearchStorage::class.java)
+
+  @Test(expected = NoSuchBeanDefinitionException::class)
+  fun doesntProvideStorageComponent_whenStorageTypeNotElasticsearch() {
+    TestPropertyValues.of("zipkin.storage.type:cassandra").applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    es()
+  }
+
+  @Test fun providesStorageComponent_whenStorageTypeElasticsearchAndHostsAreUrls() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.elasticsearch.hosts:http://host1:9200")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    assertThat(es()).isNotNull
+  }
+
+  @Test fun canOverridesProperty_hostsWithList() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.elasticsearch.hosts:http://host1:9200,http://host2:9200")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    assertThat(es().hostsSupplier().get())
+      .containsExactly("http://host1:9200", "http://host2:9200")
+  }
+
+  @Test fun configuresPipeline() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.elasticsearch.hosts:http://host1:9200",
+      "zipkin.storage.elasticsearch.pipeline:zipkin")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    assertThat(es().pipeline()).isEqualTo("zipkin")
+  }
+
+  @Test fun configuresMaxRequests() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.elasticsearch.hosts:http://host1:9200",
+      "zipkin.storage.elasticsearch.max-requests:200")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    assertThat(es().maxRequests()).isEqualTo(200)
+  }
+
+  /** This helps ensure old setups don't break (provided they have http port 9200 open)  */
+  @Test fun coersesPort9300To9200() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.elasticsearch.hosts:host1:9300")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    assertThat(es().hostsSupplier().get()).containsExactly("http://host1:9200")
+  }
+
+  @Test fun httpPrefixOptional() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.elasticsearch.hosts:host1:9200")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    assertThat(es().hostsSupplier().get()).containsExactly("http://host1:9200")
+  }
+
+  @Test fun defaultsToPort9200() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.elasticsearch.hosts:host1")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    assertThat(es().hostsSupplier().get()).containsExactly("http://host1:9200")
+  }
+
+  @Configuration
+  open class InterceptorConfiguration {
+
+    @Bean @Qualifier("zipkinElasticsearchHttp") open fun one(): Interceptor {
+      return one
+    }
+
+    @Bean @Qualifier("zipkinElasticsearchHttp") open fun two(): Interceptor {
+      return two
+    }
+
+    companion object {
+      val one: Interceptor = Interceptor { chain -> chain.proceed(chain.request()) }
+      val two: Interceptor = Interceptor { chain -> chain.proceed(chain.request()) }
+    }
+  }
+
+  /** Ensures we can wire up network interceptors, such as for logging or authentication  */
+  @Test fun usesInterceptorsQualifiedWith_zipkinElasticsearchHttp() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.elasticsearch.hosts:host1:9200")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.register(InterceptorConfiguration::class.java)
+    context.refresh()
+
+    assertThat(context.getBean(OkHttpClient::class.java).networkInterceptors())
+      .containsOnlyOnce(InterceptorConfiguration.one, InterceptorConfiguration.two)
+  }
+
+  @Test fun timeout_defaultsTo10Seconds() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.elasticsearch.hosts:host1:9200")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    val client = context.getBean(OkHttpClient::class.java)
+    assertThat(client.connectTimeoutMillis()).isEqualTo(10000)
+    assertThat(client.readTimeoutMillis()).isEqualTo(10000)
+    assertThat(client.writeTimeoutMillis()).isEqualTo(10000)
+  }
+
+  @Test fun timeout_override() {
+    val timeout = 30000
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.elasticsearch.hosts:http://host1:9200",
+      "zipkin.storage.elasticsearch.timeout:$timeout")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    val client = context.getBean(OkHttpClient::class.java)
+    assertThat(client.connectTimeoutMillis()).isEqualTo(timeout)
+    assertThat(client.readTimeoutMillis()).isEqualTo(timeout)
+    assertThat(client.writeTimeoutMillis()).isEqualTo(timeout)
+  }
+
+  @Test fun strictTraceId_defaultsToTrue() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.elasticsearch.hosts:http://host1:9200")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+    assertThat(es().strictTraceId()).isTrue()
+  }
+
+  @Test fun strictTraceId_canSetToFalse() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.elasticsearch.hosts:http://host1:9200",
+      "zipkin.storage.strict-trace-id:false")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    assertThat(es().strictTraceId()).isFalse()
+  }
+
+  @Test fun dailyIndexFormat() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.elasticsearch.hosts:http://host1:9200")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    assertThat(es().indexNameFormatter().formatTypeAndTimestamp("span", 0))
+      .isEqualTo("zipkin*span-1970-01-01")
+  }
+
+  @Test fun dailyIndexFormat_overridingPrefix() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.elasticsearch.hosts:http://host1:9200",
+      "zipkin.storage.elasticsearch.index:zipkin_prod")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    assertThat(es().indexNameFormatter().formatTypeAndTimestamp("span", 0))
+      .isEqualTo("zipkin_prod*span-1970-01-01")
+  }
+
+  @Test fun dailyIndexFormat_overridingDateSeparator() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.elasticsearch.hosts:http://host1:9200",
+      "zipkin.storage.elasticsearch.date-separator:.")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    assertThat(es().indexNameFormatter().formatTypeAndTimestamp("span", 0))
+      .isEqualTo("zipkin*span-1970.01.01")
+  }
+
+  @Test fun dailyIndexFormat_overridingDateSeparator_empty() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.elasticsearch.hosts:http://host1:9200",
+      "zipkin.storage.elasticsearch.date-separator:")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    assertThat(es().indexNameFormatter().formatTypeAndTimestamp("span", 0))
+      .isEqualTo("zipkin*span-19700101")
+  }
+
+  @Test(expected = BeanCreationException::class)
+  fun dailyIndexFormat_overridingDateSeparator_invalidToBeMultiChar() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.elasticsearch.hosts:http://host1:9200",
+      "zipkin.storage.elasticsearch.date-separator:blagho")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+
+    context.refresh()
+  }
+
+  @Test fun namesLookbackAssignedFromQueryLookback() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.elasticsearch.hosts:http://host1:9200",
+      "zipkin.query.lookback:" + TimeUnit.DAYS.toMillis(2))
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    assertThat(es().namesLookback()).isEqualTo(TimeUnit.DAYS.toMillis(2).toInt())
+  }
+
+  @Test(expected = NoSuchBeanDefinitionException::class)
+  fun doesntProvideBasicAuthInterceptor_whenBasicAuthUserNameandPasswordNotConfigured() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.elasticsearch.hosts:http://host1:9200")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    context.getBean(Interceptor::class.java)
+  }
+
+  @Test fun providesBasicAuthInterceptor_whenBasicAuthUserNameAndPasswordConfigured() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.elasticsearch.hosts:http://host1:9200",
+      "zipkin.storage.elasticsearch.username:somename",
+      "zipkin.storage.elasticsearch.password:pass")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    assertThat(context.getBean(OkHttpClient::class.java).networkInterceptors())
+      .extracting<String, RuntimeException> { i -> i.javaClass.name }
+      .contains("zipkin2.server.internal.elasticsearch.BasicAuthInterceptor")
+  }
+
+  @Test fun searchEnabled_false() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.search-enabled:false")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    assertThat(es().searchEnabled()).isFalse()
+  }
+
+  @Test fun autocompleteKeys_list() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.autocomplete-keys:environment")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    assertThat(es().autocompleteKeys())
+      .containsOnly("environment")
+  }
+
+  @Test fun autocompleteTtl() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.autocomplete-ttl:60000")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    assertThat(es().autocompleteTtl())
+      .isEqualTo(60000)
+  }
+
+  @Test fun autocompleteCardinality() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:elasticsearch",
+      "zipkin.storage.autocomplete-cardinality:5000")
+      .applyTo(context)
+    Access.registerElasticsearchHttp(context)
+    context.refresh()
+
+    assertThat(es().autocompleteCardinality())
+      .isEqualTo(5000)
+  }
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/server/internal/Http.kt b/zipkin-server/src/test/kotlin/zipkin2/server/internal/Http.kt
new file mode 100644
index 0000000..e4aa395
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/Http.kt
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.server.internal
+
+import com.linecorp.armeria.common.SessionProtocol
+import com.linecorp.armeria.server.Server
+import okhttp3.Headers
+import okhttp3.MediaType
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody
+import okhttp3.Response
+import zipkin2.TestObjects.UTF_8
+
+object Http {
+  var client: OkHttpClient = OkHttpClient.Builder().followRedirects(false).build()
+
+  fun get(server: Server, path: String, headers: Headers = Headers.of()): Response =
+    client.newCall(Request.Builder().url(url(server, path)).headers(headers).build()).execute()
+
+  fun getAsString(server: Server, path: String, headers: Headers = Headers.of()): String =
+    get(server, path, headers).body()!!.string()
+
+  fun post(
+    server: Server,
+    path: String,
+    contentType: String? = "application/json",
+    body: String,
+    headers: Headers = Headers.of()
+  ) = post(server, path, contentType, body.toByteArray(UTF_8), headers)
+
+  fun post(
+    server: Server,
+    path: String,
+    contentType: String? = "application/json",
+    body: ByteArray,
+    headers: Headers = Headers.of()
+  ): Response {
+    val body =
+      RequestBody.create(if (contentType != null) MediaType.parse(contentType) else null, body)
+    val result = client.newCall(
+      Request.Builder().url(url(server, path)).headers(headers).post(body).build()).execute()
+    return result;
+  }
+
+  fun url(server: Server, path: String, protocol: SessionProtocol = SessionProtocol.HTTP): String {
+    return server.activePorts().values.stream()
+      .filter { p -> p.hasProtocol(protocol) }.findAny()
+      .map { p -> protocol.uriText() + "://127.0.0.1:" + p.localAddress().port + path }
+      .orElseThrow { AssertionError("$protocol port not open") }
+  }
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinGrpcCollector.kt b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinGrpcCollector.kt
index 269f982..1f17b67 100644
--- a/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinGrpcCollector.kt
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinGrpcCollector.kt
@@ -33,7 +33,7 @@ import org.springframework.boot.test.context.SpringBootTest
 import org.springframework.test.context.junit4.SpringRunner
 import zipkin.server.ZipkinServer
 import zipkin2.Span
-import zipkin2.TestObjects
+import zipkin2.TestObjects.TRACE
 import zipkin2.codec.SpanBytesDecoder
 import zipkin2.codec.SpanBytesEncoder
 import zipkin2.proto3.ListOfSpans
@@ -41,17 +41,20 @@ import zipkin2.proto3.ReportResponse
 import zipkin2.storage.InMemoryStorage
 
 /** This tests that we accept messages constructed by other clients. */
-@SpringBootTest(classes = [ZipkinServer::class],
+@SpringBootTest(
+  classes = [ZipkinServer::class],
   webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
-  properties = ["spring.config.name=zipkin-server", "zipkin.collector.grpc.enabled=true"])
+  properties = ["spring.config.name=zipkin-server", "zipkin.collector.grpc.enabled=true"]
+)
 @RunWith(SpringRunner::class)
 // Written in Kotlin as Square Wire's grpc client is Kotlin.
 // Also, the first author of this test wanted an excuse to write Kotlin.
 class ITZipkinGrpcCollector {
-  @Autowired lateinit var storage: InMemoryStorage
   @Autowired lateinit var server: Server
+  @Autowired lateinit var storage: InMemoryStorage
+  @Before fun clearStorage() = storage.clear()
 
-  var request = ListOfSpans.ADAPTER.decode(SpanBytesEncoder.PROTO3.encodeList(TestObjects.TRACE))
+  val request: ListOfSpans = ListOfSpans.ADAPTER.decode(SpanBytesEncoder.PROTO3.encodeList(TRACE))
   lateinit var spanService: SpanService
 
   interface SpanService : Service {
@@ -65,7 +68,7 @@ class ITZipkinGrpcCollector {
 
   @Before fun sanityCheckCodecCompatible() {
     assertThat(SpanBytesDecoder.PROTO3.decodeList(request.encode()))
-      .containsExactlyElementsOf(TestObjects.TRACE)
+      .containsExactlyElementsOf(TRACE)
   }
 
   @Before fun createClient() {
@@ -80,7 +83,7 @@ class ITZipkinGrpcCollector {
       spanService.Report(request) // Result is effectively void
     }
     assertThat<List<Span>>(storage.traces)
-      .containsExactly(TestObjects.TRACE)
+      .containsExactly(TRACE)
   }
 
   @Test fun report_emptyIsOk() {
diff --git a/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinHttpCollector.kt b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinHttpCollector.kt
index 9a0a215..b8e10b4 100644
--- a/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinHttpCollector.kt
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinHttpCollector.kt
@@ -17,10 +17,7 @@
 package zipkin2.server.internal
 
 import com.linecorp.armeria.server.Server
-import okhttp3.MediaType
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.RequestBody
+import okhttp3.Headers
 import okhttp3.Response
 import okio.Buffer
 import okio.GzipSink
@@ -33,83 +30,71 @@ import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.boot.test.context.SpringBootTest
 import org.springframework.test.context.junit4.SpringRunner
 import zipkin.server.ZipkinServer
-import zipkin2.Span
 import zipkin2.TestObjects.TRACE
 import zipkin2.codec.SpanBytesEncoder
 import zipkin2.storage.InMemoryStorage
-import java.io.IOException
-import java.util.Arrays.asList
 
-@SpringBootTest(classes = [ZipkinServer::class],
+@SpringBootTest(
+  classes = [ZipkinServer::class],
   webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
-  properties = ["spring.config.name=zipkin-server"])
+  properties = ["spring.config.name=zipkin-server"]
+)
 @RunWith(SpringRunner::class)
 class ITZipkinHttpCollector {
-  @Autowired lateinit var storage: InMemoryStorage
   @Autowired lateinit var server: Server
+  @Autowired lateinit var storage: InMemoryStorage
+  @Before fun clearStorage() = storage.clear()
 
-  var client = OkHttpClient.Builder().followRedirects(true).build()
-
-  @Before fun init() {
-    storage.clear()
-  }
-
-  @Test @Throws(IOException::class)
-  fun noContentTypeIsJsonV2() {
-    val response = post("/api/v2/spans", SpanBytesEncoder.JSON_V2.encodeList(TRACE))
+  @Test fun noContentTypeIsJsonV2() {
+    val response = Http.post(server, "/api/v2/spans", null,
+      SpanBytesEncoder.JSON_V2.encodeList(TRACE)
+    )
 
     assertThat(response.code())
       .isEqualTo(202)
 
-    assertThat(storage.traces).containsExactly(TRACE);
+    assertThat(storage.traces).containsExactly(TRACE)
   }
 
-  @Test @Throws(IOException::class)
-  fun jsonV2() {
-    val body = SpanBytesEncoder.JSON_V2.encodeList(TRACE);
-    val response = client.newCall(Request.Builder()
-      .url(url(server, "/api/v2/spans"))
-      .post(RequestBody.create(MediaType.parse("application/json"), body))
-      .build()).execute()
+  @Test fun jsonV2() {
+    val response = Http.post(server, "/api/v2/spans", "application/json",
+      SpanBytesEncoder.JSON_V2.encodeList(TRACE)
+    )
 
     assertThat(response.code())
       .isEqualTo(202)
 
-    assertThat(storage.traces).containsExactly(TRACE);
+    assertThat(storage.traces).containsExactly(TRACE)
   }
 
-  @Test @Throws(IOException::class)
-  fun jsonV2_accidentallySentV1Format() {
-    val message = SpanBytesEncoder.JSON_V1.encodeList(TRACE)
+  @Test fun jsonV2_accidentallySentV1Format() {
+    val response = Http.post(server, "/api/v2/spans", "application/json",
+      SpanBytesEncoder.JSON_V1.encodeList(TRACE)
+    )
 
-    val response = post("/api/v2/spans", message)
     assertThat(response.code()).isEqualTo(400)
     assertThat(response.body()!!.string())
       .startsWith("Expected a JSON_V2 encoded list, but received: JSON_V1\n")
   }
 
-  @Test @Throws(IOException::class)
-  fun jsonV1_accidentallySentV2Format() {
-    val message = SpanBytesEncoder.JSON_V2.encodeList(TRACE)
+  @Test fun jsonV1_accidentallySentV2Format() {
+    val response = Http.post(server, "/api/v1/spans", "application/json",
+      SpanBytesEncoder.JSON_V2.encodeList(TRACE)
+    )
 
-    val response = post("/api/v1/spans", message)
     assertThat(response.code()).isEqualTo(400)
     assertThat(response.body()!!.string())
       .startsWith("Expected a JSON_V1 encoded list, but received: JSON_V2\n")
   }
 
-  @Test @Throws(IOException::class)
-  fun ambiguousFormatOk() {
-    val message = SpanBytesEncoder.JSON_V2.encodeList(asList(
-      Span.newBuilder().traceId("1").id("1").name("test").build()
-    ))
+  @Test fun ambiguousFormatOk() {
+    val body = """[{"traceId": "1", "id": "1", "name": "test"}]"""
 
-    assertThat(post("/api/v1/spans", message).code()).isEqualTo(202)
-    assertThat(post("/api/v2/spans", message).code()).isEqualTo(202)
+    assertThat(Http.post(server, "/api/v1/spans", null, body).code()).isEqualTo(202)
+    assertThat(Http.post(server, "/api/v2/spans", null, body).code()).isEqualTo(202)
   }
 
-  @Test @Throws(IOException::class)
-  fun emptyIsOk() {
+  @Test fun emptyIsOk() {
     assertOnAllEndpoints(byteArrayOf()) { response: Response,
       path: String, contentType: String, encoding: String ->
       assertThat(response.isSuccessful)
@@ -118,8 +103,7 @@ class ITZipkinHttpCollector {
     }
   }
 
-  @Test @Throws(IOException::class)
-  fun malformedNotOk() {
+  @Test fun malformedNotOk() {
     assertOnAllEndpoints(byteArrayOf(1, 2, 3, 4)) { response: Response,
       path: String, contentType: String, encoding: String ->
       assertThat(response.code()).isEqualTo(400)
@@ -148,56 +132,40 @@ class ITZipkinHttpCollector {
     ).forEach {
       val (path, contentType) = it
       for (encoding in listOf("identity", "gzip")) {
-        val response = client.newCall(Request.Builder()
-          .url(url(server, path))
-          .header("Content-Encoding", encoding)
-          .post(RequestBody.create(MediaType.get(contentType), body))
-          .build()).execute()
+        val response =
+          Http.post(server, path, contentType, body, Headers.of("Content-Encoding", encoding))
 
         assertion(response, path, contentType, encoding)
       }
     }
   }
 
-  @Test @Throws(IOException::class)
-  fun contentTypeXThrift() {
-    val message = SpanBytesEncoder.THRIFT.encodeList(TRACE)
-
-    val response = client.newCall(Request.Builder()
-      .url(url(server, "/api/v1/spans"))
-      .post(RequestBody.create(MediaType.parse("application/x-thrift"), message))
-      .build()).execute()
+  @Test fun contentTypeXThrift() {
+    val response = Http.post(server, "/api/v1/spans", "application/x-thrift",
+      SpanBytesEncoder.THRIFT.encodeList(TRACE)
+    )
 
     assertThat(response.code())
-      .withFailMessage(response.body()!!.string())
       .isEqualTo(202)
   }
 
-  @Test @Throws(IOException::class)
-  fun contentTypeXProtobuf() {
-    val message = SpanBytesEncoder.PROTO3.encodeList(TRACE)
-
-    val response = client.newCall(Request.Builder()
-      .url(url(server, "/api/v2/spans"))
-      .post(RequestBody.create(MediaType.parse("application/x-protobuf"), message))
-      .build()).execute()
+  @Test fun contentTypeXProtobuf() {
+    val response = Http.post(server, "/api/v2/spans", "application/x-protobuf",
+      SpanBytesEncoder.PROTO3.encodeList(TRACE)
+    )
 
     assertThat(response.code())
       .isEqualTo(202)
   }
 
-  @Test @Throws(IOException::class)
-  fun gzipEncoded() {
-    val message = SpanBytesEncoder.JSON_V2.encodeList(TRACE)
-    val gzippedBody = gzip(message)
-
-    val response = client.newCall(Request.Builder()
-      .url(url(server, "/api/v2/spans"))
-      .header("Content-Encoding", "gzip")
-      .post(RequestBody.create(null, gzippedBody))
-      .build()).execute()
+  @Test fun gzipEncoded() {
+    val response = Http.post(server, "/api/v2/spans", null,
+      gzip(SpanBytesEncoder.JSON_V2.encodeList(TRACE)),
+      Headers.of("Content-Encoding", "gzip")
+    )
 
-    assertThat(response.isSuccessful)
+    assertThat(response.code())
+      .isEqualTo(202)
   }
 
   fun gzip(message: ByteArray): ByteArray {
@@ -205,26 +173,6 @@ class ITZipkinHttpCollector {
     val gzipSink = GzipSink(sink)
     gzipSink.write(Buffer().write(message), message.size.toLong())
     gzipSink.close()
-    val gzippedBody = sink.readByteArray()
-    return gzippedBody
-  }
-
-  @Throws(IOException::class)
-  fun get(path: String): Response {
-    return client.newCall(Request.Builder()
-      .url(url(server, path))
-      .build()).execute()
-  }
-
-  @Throws(IOException::class)
-  fun post(path: String, body: ByteArray): Response {
-    return client.newCall(Request.Builder()
-      .url(url(server, path))
-      .post(RequestBody.create(null, body))
-      .build()).execute()
-  }
-
-  fun url(server: Server, path: String): String {
-    return "http://localhost:" + server.activePort().get().localAddress().port + path
+    return sink.readByteArray()
   }
 }
diff --git a/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinMetricsHealth.kt b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinMetricsHealth.kt
new file mode 100644
index 0000000..fcd4dc1
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinMetricsHealth.kt
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.server.internal
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.jayway.jsonpath.JsonPath
+import com.linecorp.armeria.server.Server
+import io.micrometer.prometheus.PrometheusMeterRegistry
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.test.context.junit4.SpringRunner
+import zipkin.server.ZipkinServer
+import zipkin2.TestObjects.LOTS_OF_SPANS
+import zipkin2.TestObjects.UTF_8
+import zipkin2.codec.SpanBytesEncoder
+import java.util.ArrayList
+
+@SpringBootTest(
+  classes = [ZipkinServer::class],
+  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+  properties = ["spring.config.name=zipkin-server"]
+)
+@RunWith(SpringRunner::class)
+class ITZipkinMetricsHealth {
+  @Autowired lateinit var server: Server
+  @Autowired lateinit var registry: PrometheusMeterRegistry
+
+  @Test fun healthIsOK() {
+    assertThat(Http.get(server, "/health").isSuccessful)
+      .isTrue()
+
+    // ensure we don't track health in prometheus
+    assertThat(scrape())
+      .doesNotContain("health")
+  }
+
+  @Test fun metricsIsOK() {
+    assertThat(Http.get(server, "/metrics").isSuccessful)
+      .isTrue()
+
+    // ensure we don't track metrics in prometheus
+    assertThat(scrape())
+      .doesNotContain("metrics")
+  }
+
+  @Test fun actuatorIsOK() {
+    assertThat(Http.get(server, "/actuator").isSuccessful)
+      .isTrue()
+
+    // ensure we don't track actuator in prometheus
+    assertThat(scrape())
+      .doesNotContain("actuator")
+  }
+
+  @Test fun prometheusIsOK() {
+    val response = Http.get(server, "/prometheus")
+    assertThat(response.code()).isEqualTo(307)
+    assertThat(response.header("location")).isEqualTo("/actuator/prometheus")
+
+    assertThat(Http.get(server, "/actuator/prometheus").isSuccessful)
+      .isTrue()
+
+    // ensure we don't track prometheus, UI requests in prometheus
+    assertThat(scrape())
+      .doesNotContain("prometheus")
+      .doesNotContain("uri=\"/zipkin")
+      .doesNotContain("uri=\"/\"")
+  }
+
+  @Test fun notFound_prometheus() {
+    assertThat(Http.get(server, "/doo-wop").isSuccessful)
+      .isFalse()
+
+    assertThat(scrape())
+      .contains("uri=\"NOT_FOUND\"")
+      .doesNotContain("uri=\"/doo-wop")
+  }
+
+  @Test fun redirected_prometheus() {
+    assertThat(Http.get(server, "/").code())
+      .isEqualTo(302) // redirect
+
+    assertThat(scrape())
+      .contains("uri=\"REDIRECTION\"")
+      .doesNotContain("uri=\"/\"")
+  }
+
+  @Test fun apiTemplate_prometheus() {
+    val spans = listOf(LOTS_OF_SPANS[0])
+    val body = SpanBytesEncoder.JSON_V2.encodeList(spans)
+    assertThat(Http.post(server, "/api/v2/spans", body = body).isSuccessful)
+      .isTrue()
+
+    assertThat(Http.get(server, "/api/v2/trace/" + LOTS_OF_SPANS[0].traceId()).isSuccessful)
+      .isTrue()
+
+    assertThat(scrape())
+      .contains("uri=\"/api/v2/trace/{traceId}\"")
+      .doesNotContain(LOTS_OF_SPANS[0].traceId())
+  }
+
+  @Test fun forwardedRoute_prometheus() {
+    assertThat(Http.get(server, "/zipkin/api/v2/services").isSuccessful)
+      .isTrue()
+
+    assertThat(scrape())
+      .contains("uri=\"/api/v2/services\"")
+      .doesNotContain("uri=\"/zipkin/api/v2/services\"")
+  }
+
+  internal fun scrape(): String {
+    Thread.sleep(100)
+    return registry.scrape()
+  }
+
+  /** Makes sure the prometheus filter doesn't count twice  */
+  @Test fun writeSpans_updatesPrometheusMetrics() {
+    val spans = listOf(LOTS_OF_SPANS[0], LOTS_OF_SPANS[1], LOTS_OF_SPANS[2])
+    val body = SpanBytesEncoder.JSON_V2.encodeList(spans)
+
+    Http.post(server, "/api/v2/spans", body = body)
+    Http.post(server, "/api/v2/spans", body = body)
+
+    Thread.sleep(100) // sometimes travis flakes getting the "http.server.requests" timer
+    val messagesCount = registry.counter("zipkin_collector.spans", "transport", "http").count()
+    // Get the http count from the registry and it should match the summation previous count
+    // and count of calls below
+    val httpCount = registry
+      .find("http.server.requests")
+      .tag("uri", "/api/v2/spans")
+      .timer()!!
+      .count()
+
+    // ensure unscoped counter does not exist
+    assertThat(scrape())
+      .doesNotContain("zipkin_collector_spans_total $messagesCount")
+      .contains("zipkin_collector_spans_total{transport=\"http\",} $messagesCount")
+      .contains(
+        "http_server_requests_seconds_count{method=\"POST\",status=\"202\",uri=\"/api/v2/spans\",} $httpCount")
+  }
+
+  @Test fun readsHealth() {
+    val json = Http.getAsString(server, "/health")
+    assertThat(readString(json, "$.status"))
+      .isIn("UP", "DOWN", "UNKNOWN")
+    assertThat(readString(json, "$.zipkin.status"))
+      .isIn("UP", "DOWN", "UNKNOWN")
+  }
+
+  @Test fun writesSpans_readMetricsFormat() {
+    val span = "zipkin".toByteArray(UTF_8)
+    val spans = listOf(LOTS_OF_SPANS[0], LOTS_OF_SPANS[1], LOTS_OF_SPANS[2])
+    val body = SpanBytesEncoder.JSON_V2.encodeList(spans)
+    Http.post(server, "/api/v2/spans", body = body)
+    Http.post(server, "/api/v2/spans", body = body)
+    Http.post(server, "/api/v2/spans", body = span)
+    Thread.sleep(1500)
+
+    assertThat(readJson(Http.getAsString(server, "/metrics")))
+      .containsExactlyInAnyOrder(
+        "gauge.zipkin_collector.message_spans.http",
+        "gauge.zipkin_collector.message_bytes.http", "counter.zipkin_collector.messages.http",
+        "counter.zipkin_collector.bytes.http", "counter.zipkin_collector.spans.http",
+        "counter.zipkin_collector.messages_dropped.http",
+        "counter.zipkin_collector.spans_dropped.http"
+      )
+  }
+
+  fun readString(json: String, jsonPath: String): String = JsonPath.compile(jsonPath).read(json)
+
+  fun readJson(json: String): List<*> {
+    val mapper = ObjectMapper()
+    val jsonNode = mapper.readTree(json)
+    val fieldsList = ArrayList<String>()
+    jsonNode.fieldNames().forEachRemaining { fieldsList.add(it) }
+    return fieldsList
+  }
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinMetricsHealthDirty.kt b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinMetricsHealthDirty.kt
new file mode 100644
index 0000000..8cbfad9
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinMetricsHealthDirty.kt
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.server.internal
+
+import com.jayway.jsonpath.JsonPath
+import com.linecorp.armeria.server.Server
+import io.micrometer.prometheus.PrometheusMeterRegistry
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.test.annotation.DirtiesContext
+import org.springframework.test.annotation.DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD
+import org.springframework.test.context.junit4.SpringRunner
+import zipkin.server.ZipkinServer
+import zipkin2.TestObjects.LOTS_OF_SPANS
+import zipkin2.codec.SpanBytesEncoder
+
+/**
+ * We cannot clear the micrometer registry easily, so we have recreate the spring context. This is
+ * extremely slow, so please only add tests that require isolation here.
+ */
+@SpringBootTest(
+  classes = [ZipkinServer::class],
+  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+  properties = ["spring.config.name=zipkin-server"]
+)
+@RunWith(SpringRunner::class)
+@DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD)
+class ITZipkinMetricsHealthDirty {
+  @Autowired lateinit var server: Server
+  @Autowired lateinit var registry: PrometheusMeterRegistry
+
+  @Test fun writeSpans_updatesMetrics() {
+    val spans = listOf(LOTS_OF_SPANS[0], LOTS_OF_SPANS[1], LOTS_OF_SPANS[2])
+    val body = SpanBytesEncoder.JSON_V2.encodeList(spans)
+    val messagesCount = registry.counter("zipkin_collector.messages", "transport", "http").count()
+    val bytesCount = registry.counter("zipkin_collector.bytes", "transport", "http").count()
+    val spansCount = registry.counter("zipkin_collector.spans", "transport", "http").count()
+    Http.post(server, "/api/v2/spans", body = body)
+    Http.post(server, "/api/v2/spans", body = body)
+
+    val json = Http.getAsString(server, "/metrics")
+
+    assertThat(readDouble(json, "$.['counter.zipkin_collector.messages.http']"))
+      .isEqualTo(messagesCount + 2.0)
+    assertThat(readDouble(json, "$.['counter.zipkin_collector.bytes.http']"))
+      .isEqualTo(bytesCount + body.size * 2)
+    assertThat(readDouble(json, "$.['gauge.zipkin_collector.message_bytes.http']"))
+      .isEqualTo(body.size.toDouble())
+    assertThat(readDouble(json, "$.['counter.zipkin_collector.spans.http']"))
+      .isEqualTo(spansCount + spans.size * 2)
+    assertThat(readDouble(json, "$.['gauge.zipkin_collector.message_spans.http']"))
+      .isEqualTo(spans.size.toDouble())
+  }
+
+  @Test fun writeSpans_malformedUpdatesMetrics() {
+    val body = byteArrayOf('h'.toByte(), 'e'.toByte(), 'l'.toByte(), 'l'.toByte(), 'o'.toByte())
+    val messagesCount = registry.counter("zipkin_collector.messages", "transport", "http").count()
+    val messagesDroppedCount =
+      registry.counter("zipkin_collector.messages_dropped", "transport", "http").count()
+    Http.post(server, "/api/v2/spans", body = body)
+
+    val json = Http.getAsString(server, "/metrics")
+
+    assertThat(readDouble(json, "$.['counter.zipkin_collector.messages.http']"))
+      .isEqualTo(messagesCount + 1)
+    assertThat(readDouble(json, "$.['counter.zipkin_collector.messages_dropped.http']"))
+      .isEqualTo(messagesDroppedCount + 1)
+  }
+
+  fun readDouble(json: String, jsonPath: String): Double = JsonPath.compile(jsonPath).read(json)
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinServer.kt b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinServer.kt
new file mode 100644
index 0000000..8612890
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinServer.kt
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.server.internal
+
+import com.linecorp.armeria.server.Server
+import okhttp3.Headers
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.test.context.junit4.SpringRunner
+import zipkin.server.ZipkinServer
+import zipkin2.Endpoint
+import zipkin2.Span
+import zipkin2.TestObjects.TODAY
+import zipkin2.TestObjects.TRACE
+import zipkin2.TestObjects.UTF_8
+import zipkin2.codec.SpanBytesEncoder
+import zipkin2.storage.InMemoryStorage
+
+@SpringBootTest(
+  classes = [ZipkinServer::class],
+  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+  properties = ["spring.config.name=zipkin-server"]
+)
+@RunWith(SpringRunner::class)
+class ITZipkinServer {
+  @Autowired lateinit var server: Server
+  @Autowired lateinit var storage: InMemoryStorage
+  @Before fun clearStorage() = storage.clear()
+
+  @Test fun getTrace() {
+    storage.accept(TRACE).execute()
+
+    val response = Http.get(server, "/api/v2/trace/" + TRACE[0].traceId())
+    assertThat(response.isSuccessful).isTrue()
+
+    assertThat(response.body()!!.bytes())
+      .containsExactly(*SpanBytesEncoder.JSON_V2.encodeList(TRACE))
+  }
+
+  @Test fun tracesQueryRequiresNoParameters() {
+    storage.accept(TRACE).execute()
+
+    val response = Http.get(server, "/api/v2/traces")
+    assertThat(response.isSuccessful).isTrue()
+    assertThat(response.body()!!.string())
+      .isEqualTo("[" + String(SpanBytesEncoder.JSON_V2.encodeList(TRACE), UTF_8) + "]")
+  }
+
+  @Test fun v2WiresUp() {
+    assertThat(Http.get(server, "/api/v2/services").isSuccessful)
+      .isTrue()
+  }
+
+  @Test fun doesntSetCacheControlOnNameEndpointsWhenLessThan4Services() {
+    storage.accept(TRACE).execute()
+
+    assertThat(Http.get(server, "/api/v2/services").header("Cache-Control"))
+      .isNull()
+
+    assertThat(Http.get(server, "/api/v2/spans?serviceName=web").header("Cache-Control"))
+      .isNull()
+
+    assertThat(Http.get(server, "/api/v2/remoteServices?serviceName=web").header("Cache-Control"))
+      .isNull()
+  }
+
+  @Test fun spanNameQueryWorksWithNonAsciiServiceName() {
+    assertThat(Http.get(server, "/api/v2/spans?serviceName=个人信息服务").code())
+      .isEqualTo(200)
+  }
+
+  @Test fun remoteServiceNameQueryWorksWithNonAsciiServiceName() {
+    assertThat(Http.get(server, "/api/v2/remoteServices?serviceName=个人信息服务").code())
+      .isEqualTo(200)
+  }
+
+  @Test fun setsCacheControlOnNameEndpointsWhenMoreThan3Services() {
+    val services = listOf("foo", "bar", "baz", "quz")
+    for (i in services.indices) {
+      storage.accept(listOf(
+        Span.newBuilder().traceId("a").id((i + 1).toLong()).timestamp(TODAY).name("whopper")
+          .localEndpoint(Endpoint.newBuilder().serviceName(services[i]).build())
+          .remoteEndpoint(Endpoint.newBuilder().serviceName(services[i] + 1).build())
+          .build()
+      )).execute()
+    }
+
+    assertThat(Http.get(server, "/api/v2/services").header("Cache-Control"))
+      .isEqualTo("max-age=300, must-revalidate")
+
+    assertThat(Http.get(server, "/api/v2/spans?serviceName=web").header("Cache-Control"))
+      .isEqualTo("max-age=300, must-revalidate")
+
+    assertThat(Http.get(server, "/api/v2/remoteServices?serviceName=web").header("Cache-Control"))
+      .isEqualTo("max-age=300, must-revalidate")
+
+    // Check that the response is alphabetically sorted.
+    assertThat(Http.getAsString(server, "/api/v2/services"))
+      .isEqualTo("[\"bar\",\"baz\",\"foo\",\"quz\"]")
+  }
+
+  @Test fun shouldAllowAnyOriginByDefault() {
+    val response = Http.get(server, "/api/v2/traces", Headers.of(
+      "Origin", "http://foo.example.com"
+    ))
+
+    assertThat(response.isSuccessful).isTrue()
+    assertThat(response.header("vary")).isNull()
+    assertThat(response.header("access-control-allow-credentials")).isNull()
+    assertThat(response.header("access-control-allow-origin")).contains("*")
+  }
+
+  @Test fun forwardsApiForUi() {
+    assertThat(Http.get(server, "/zipkin/api/v2/traces").isSuccessful).isTrue()
+    assertThat(Http.get(server, "/zipkin/api/v2/traces").isSuccessful).isTrue()
+  }
+
+  /** Simulate a proxy which forwards / to zipkin as opposed to resolving / -> /zipkin first  */
+  @Test fun redirectedHeaderUsesOriginalHostAndPort() {
+    val response = Http.get(server, "/", Headers.of(
+      "Host", "zipkin.com",
+      "X-Forwarded-Proto", "https",
+      "X-Forwarded-Port", "444"
+    ))
+
+    // Redirect header should be the proxy, not the backed IP/port
+    assertThat(response.header("Location"))
+      .isEqualTo("/zipkin/")
+  }
+
+  @Test fun infoEndpointIsAvailable() {
+    val response = Http.get(server, "/info")
+    assertThat(response.code()).isEqualTo(307)
+    assertThat(response.header("location")).isEqualTo("/actuator/info")
+
+    assertThat(Http.get(server, "/actuator/info").isSuccessful).isTrue()
+  }
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinServerAutocomplete.kt b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinServerAutocomplete.kt
new file mode 100644
index 0000000..4e04983
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinServerAutocomplete.kt
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.server.internal
+
+import com.linecorp.armeria.server.Server
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.test.context.junit4.SpringRunner
+import zipkin.server.ZipkinServer
+import zipkin2.Span
+import zipkin2.TestObjects.TODAY
+import zipkin2.codec.SpanBytesEncoder
+
+/**
+ * Integration test suite for autocomplete tags.
+ *
+ * Verifies that the whitelist of key can be configured via "zipkin.storage.autocomplete-keys".
+ */
+@SpringBootTest(
+  classes = [ZipkinServer::class],
+  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+  properties = [
+    "spring.config.name=zipkin-server",
+    "zipkin.storage.autocomplete-keys=environment,clnt/finagle.version"
+  ]
+)
+@RunWith(SpringRunner::class)
+class ITZipkinServerAutocomplete {
+  @Autowired lateinit var server: Server
+  val values = "/api/v2/autocompleteValues"
+
+  @Test fun setsCacheControlOnAutocompleteKeysEndpoint() {
+    assertThat(Http.get(server, "/api/v2/autocompleteKeys").header("Cache-Control"))
+      .isEqualTo("max-age=300, must-revalidate")
+  }
+
+  @Test fun setsCacheControlOnAutocompleteEndpointWhenMoreThan3Values() {
+    assertThat(Http.get(server, "$values?key=environment").header("Cache-Control"))
+      .isNull()
+    assertThat(Http.get(server, "$values?key=clnt/finagle.version").header("Cache-Control"))
+      .isNull()
+
+    for (i in 0..3) {
+      Http.post(server, "/api/v2/spans", body = SpanBytesEncoder.JSON_V2.encodeList(listOf(
+        Span.newBuilder().traceId("a").id((i + 1).toLong()).timestamp(TODAY).name("whopper")
+          .putTag("clnt/finagle.version", "6.45.$i").build()
+      )))
+    }
+
+    assertThat(Http.get(server, "$values?key=environment").header("Cache-Control"))
+      .isNull()
+    assertThat(Http.get(server, "$values?key=clnt/finagle.version").header("Cache-Control"))
+      .isEqualTo("max-age=300, must-revalidate")
+  }
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinServerCORS.kt b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinServerCORS.kt
new file mode 100644
index 0000000..e2d4485
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinServerCORS.kt
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.server.internal
+
+import com.linecorp.armeria.server.Server
+import okhttp3.Headers
+import okhttp3.Request
+import okhttp3.Response
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.test.context.junit4.SpringRunner
+import zipkin.server.ZipkinServer
+
+/**
+ * Integration test suite for CORS configuration.
+ *
+ * Verifies that allowed-origins can be configured via properties (zipkin.query.allowed-origins).
+ */
+@SpringBootTest(classes = [ZipkinServer::class],
+  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+  properties = [
+    "spring.config.name=zipkin-server",
+    "zipkin.query.allowed-origins=http://foo.example.com"
+  ])
+@RunWith(SpringRunner::class)
+class ITZipkinServerCORS {
+  @Autowired lateinit var server: Server
+  @Autowired @Value("\${zipkin.query.allowed-origins}")
+  lateinit var allowedOrigin: String
+  internal val disallowedOrigin = "http://bar.example.com"
+
+  /** Notably, javascript makes pre-flight requests, and won't POST spans if disallowed!  */
+  @Test fun shouldAllowConfiguredOrigin_preflight() {
+    shouldPermitPreflight(optionsForOrigin("GET", "/api/v2/traces", allowedOrigin))
+    shouldPermitPreflight(optionsForOrigin("POST", "/api/v2/spans", allowedOrigin))
+  }
+
+  @Test fun shouldAllowConfiguredOrigin() {
+    shouldAllowConfiguredOrigin(getTracesFromOrigin(allowedOrigin))
+    shouldAllowConfiguredOrigin(postSpansFromOrigin(allowedOrigin))
+  }
+
+  @Test fun shouldDisallowOrigin() {
+    shouldDisallowOrigin(getTracesFromOrigin(disallowedOrigin))
+    shouldDisallowOrigin(postSpansFromOrigin(disallowedOrigin))
+  }
+
+  fun optionsForOrigin(method: String, path: String, origin: String): Response =
+    Http.client.newCall(Request.Builder().url(Http.url(server, path)).headers(Headers.of(
+      "Origin", origin,
+      "access-control-request-method", method,
+      "access-control-request-headers", "content-type"))
+      .method("OPTIONS", null)
+      .build()).execute()
+
+  fun getTracesFromOrigin(origin: String): Response =
+    Http.get(server, "/api/v2/traces", Headers.of("Origin", origin))
+
+  fun postSpansFromOrigin(origin: String): Response =
+    Http.post(server, "/api/v2/traces", null, "[]", Headers.of("Origin", origin))
+
+  fun shouldPermitPreflight(response: Response) {
+    assertThat(response.isSuccessful)
+      .withFailMessage(response.toString())
+      .isTrue()
+    assertThat(response.header("vary")).contains("origin")
+    assertThat(response.header("access-control-allow-origin")).contains(allowedOrigin)
+    assertThat(response.header("access-control-allow-methods"))
+      .contains(response.request().header("access-control-request-method"))
+    assertThat(response.header("access-control-allow-credentials")).isNull()
+    assertThat(response.header("access-control-allow-headers")).contains("content-type")
+  }
+
+  fun shouldAllowConfiguredOrigin(response: Response) {
+    assertThat(response.header("vary")).contains("origin")
+    assertThat(response.header("access-control-allow-origin"))
+      .contains(response.request().header("origin"))
+    assertThat(response.header("access-control-allow-credentials")).isNull()
+    assertThat(response.header("access-control-allow-headers")).contains("content-type")
+  }
+
+  fun shouldDisallowOrigin(response: Response) {
+    assertThat(response.header("vary")).isNull() // TODO: We used to set vary: origin
+    assertThat(response.header("access-control-allow-credentials")).isNull()
+    assertThat(response.header("access-control-allow-origin")).isNull()
+    assertThat(response.header("access-control-allow-headers")).isNull()
+  }
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinServerHttpCollectorDisabled.kt b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinServerHttpCollectorDisabled.kt
new file mode 100644
index 0000000..f355af9
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinServerHttpCollectorDisabled.kt
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.server.internal
+
+import com.linecorp.armeria.server.Server
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.test.context.junit4.SpringRunner
+import zipkin.server.ZipkinServer
+
+/**
+ * Query-only builds should be able to disable the HTTP collector, so that associated assets 404
+ * instead of allowing creation of spans.
+ */
+@SpringBootTest(
+  classes = [ZipkinServer::class],
+  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+  properties = ["spring.config.name=zipkin-server", "zipkin.collector.http.enabled=false"]
+)
+@RunWith(SpringRunner::class)
+class ITZipkinServerHttpCollectorDisabled {
+  @Autowired lateinit var server: Server
+
+  @Test fun httpCollectorEndpointReturns404() {
+    val response = Http.post(server, "/api/v2/spans", body = "[]")
+
+    assertThat(response.code()).isEqualTo(404)
+  }
+
+  /** Shows the same http path still works for GET  */
+  @Test fun getOnSpansEndpointReturnsOK() {
+    val response = Http.get(server, "/api/v2/spans?serviceName=unknown")
+
+    assertThat(response.isSuccessful).isTrue()
+  }
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinServerQueryDisabled.kt b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinServerQueryDisabled.kt
new file mode 100644
index 0000000..97a5831
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinServerQueryDisabled.kt
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.server.internal
+
+import com.linecorp.armeria.server.Server
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.test.context.junit4.SpringRunner
+import zipkin.server.ZipkinServer
+
+/**
+ * Collector-only builds should be able to disable the query (and indirectly the UI), so that
+ * associated assets 404 vs throw exceptions.
+ */
+@SpringBootTest(
+  classes = [ZipkinServer::class],
+  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+  properties = [
+    "spring.config.name=zipkin-server",
+    "zipkin.query.enabled=false",
+    "zipkin.ui.enabled=false"
+  ]
+)
+@RunWith(SpringRunner::class)
+class ITZipkinServerQueryDisabled {
+  @Autowired lateinit var server: Server
+
+  @Test fun queryRelatedEndpoints404() {
+    assertThat(Http.get(server, "/api/v2/traces").code()).isEqualTo(404)
+    assertThat(Http.get(server, "/index.html").code()).isEqualTo(404)
+
+    // but other endpoints are ok
+    assertThat(Http.get(server, "/health").isSuccessful).isTrue()
+  }
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinServerSsl.kt b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinServerSsl.kt
new file mode 100644
index 0000000..76cb0f8
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ITZipkinServerSsl.kt
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.server.internal
+
+import com.linecorp.armeria.client.ClientFactoryBuilder
+import com.linecorp.armeria.client.HttpClient
+import com.linecorp.armeria.common.HttpStatus
+import com.linecorp.armeria.common.SessionProtocol
+import com.linecorp.armeria.server.Server
+import io.netty.handler.ssl.util.InsecureTrustManagerFactory
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.test.context.junit4.SpringRunner
+import zipkin.server.ZipkinServer
+
+/**
+ * This code ensures you can setup SSL.
+ *
+ *
+ * This is inspired by com.linecorp.armeria.spring.ArmeriaSslConfigurationTest
+ */
+@SpringBootTest(
+  classes = [ZipkinServer::class],
+  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+  properties = [
+    "spring.config.name=zipkin-server",
+    "armeria.ssl.enabled=true",
+    "armeria.ports[1].port=0",
+    "armeria.ports[1].protocols[0]=https",
+    // redundant in zipkin-server-shared https://github.com/spring-projects/spring-boot/issues/16394
+    "armeria.ports[0].port=\${server.port}",
+    "armeria.ports[0].protocols[0]=http"
+  ]
+)
+@RunWith(SpringRunner::class)
+class ITZipkinServerSsl {
+  @Autowired lateinit var server: Server
+
+  // We typically use OkHttp in our tests, but Armeria bundles a handy insecure trust manager
+  internal val clientFactory = ClientFactoryBuilder()
+    .sslContextCustomizer { b -> b.trustManager(InsecureTrustManagerFactory.INSTANCE) }
+    .build()
+
+  @Test fun callHealthEndpoint_HTTP() {
+    callHealthEndpoint(SessionProtocol.HTTP)
+  }
+
+  @Test fun callHealthEndpoint_HTTPS() {
+    callHealthEndpoint(SessionProtocol.HTTPS)
+  }
+
+  fun callHealthEndpoint(protocol: SessionProtocol) {
+    val response = HttpClient.of(clientFactory, Http.url(server, "", protocol)).get("/health")
+      .aggregate().join()
+
+    assertThat(response.status()).isEqualTo(HttpStatus.OK)
+  }
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/server/internal/InMemoryCollectorConfiguration.kt b/zipkin-server/src/test/kotlin/zipkin2/server/internal/InMemoryCollectorConfiguration.kt
new file mode 100644
index 0000000..6afa971
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/InMemoryCollectorConfiguration.kt
@@ -0,0 +1,23 @@
+package zipkin2.server.internal
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import zipkin2.collector.CollectorMetrics
+import zipkin2.collector.CollectorSampler
+import zipkin2.storage.InMemoryStorage
+import zipkin2.storage.StorageComponent
+
+@Configuration
+open class InMemoryCollectorConfiguration {
+  @Bean open fun sampler(): CollectorSampler {
+    return CollectorSampler.ALWAYS_SAMPLE
+  }
+
+  @Bean open fun metrics(): CollectorMetrics {
+    return CollectorMetrics.NOOP_METRICS
+  }
+
+  @Bean open fun storage(): StorageComponent {
+    return InMemoryStorage.newBuilder().build()
+  }
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/server/internal/ZipkinServerConfigurationTest.kt b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ZipkinServerConfigurationTest.kt
new file mode 100644
index 0000000..1455a8d
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ZipkinServerConfigurationTest.kt
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.server.internal
+
+import brave.Tracing
+import com.linecorp.armeria.spring.actuate.ArmeriaSpringActuatorAutoConfiguration
+import io.micrometer.core.instrument.MeterRegistry
+import io.micrometer.prometheus.PrometheusConfig
+import io.micrometer.prometheus.PrometheusMeterRegistry
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown
+import org.junit.After
+import org.junit.Test
+import org.springframework.beans.factory.NoSuchBeanDefinitionException
+import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration
+import org.springframework.boot.actuate.health.HealthAggregator
+import org.springframework.boot.actuate.health.OrderedHealthAggregator
+import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
+import org.springframework.boot.test.util.TestPropertyValues
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import zipkin2.server.internal.brave.TracingConfiguration
+import zipkin2.storage.StorageComponent
+
+class ZipkinServerConfigurationTest {
+  val context = AnnotationConfigApplicationContext()
+  @After fun closeContext() = context.close()
+
+  @Test fun httpCollector_enabledByDefault() {
+    context.register(
+      ArmeriaSpringActuatorAutoConfiguration::class.java,
+      EndpointAutoConfiguration::class.java,
+      PropertyPlaceholderAutoConfiguration::class.java,
+      ZipkinServerConfigurationTest.Config::class.java,
+      ZipkinServerConfiguration::class.java,
+      ZipkinHttpCollector::class.java
+    )
+    context.refresh()
+
+    assertThat(context.getBean(ZipkinHttpCollector::class.java)).isNotNull
+  }
+
+  @Test(expected = NoSuchBeanDefinitionException::class)
+  fun httpCollector_canDisable() {
+    TestPropertyValues.of("zipkin.collector.http.enabled:false").applyTo(context)
+    context.register(
+      ArmeriaSpringActuatorAutoConfiguration::class.java,
+      EndpointAutoConfiguration::class.java,
+      PropertyPlaceholderAutoConfiguration::class.java,
+      ZipkinServerConfigurationTest.Config::class.java,
+      ZipkinServerConfiguration::class.java,
+      ZipkinHttpCollector::class.java
+    )
+    context.refresh()
+
+    context.getBean(ZipkinHttpCollector::class.java)
+  }
+
+  @Test fun query_enabledByDefault() {
+    context.register(
+      ArmeriaSpringActuatorAutoConfiguration::class.java,
+      EndpointAutoConfiguration::class.java,
+      PropertyPlaceholderAutoConfiguration::class.java,
+      ZipkinServerConfigurationTest.Config::class.java,
+      ZipkinServerConfiguration::class.java,
+      ZipkinQueryApiV2::class.java
+    )
+    context.refresh()
+
+    assertThat(context.getBean(ZipkinQueryApiV2::class.java)).isNotNull
+  }
+
+  @Test fun query_canDisable() {
+    TestPropertyValues.of("zipkin.query.enabled:false").applyTo(context)
+    context.register(
+      ArmeriaSpringActuatorAutoConfiguration::class.java,
+      EndpointAutoConfiguration::class.java,
+      PropertyPlaceholderAutoConfiguration::class.java,
+      ZipkinServerConfigurationTest.Config::class.java,
+      ZipkinServerConfiguration::class.java,
+      ZipkinQueryApiV2::class.java
+    )
+    context.refresh()
+
+    try {
+      context.getBean(ZipkinQueryApiV2::class.java)
+      failBecauseExceptionWasNotThrown<Any>(NoSuchBeanDefinitionException::class.java)
+    } catch (e: NoSuchBeanDefinitionException) {
+    }
+
+  }
+
+  @Test fun selfTracing_canEnable() {
+    TestPropertyValues.of("zipkin.self-tracing.enabled:true").applyTo(context)
+    context.register(
+      ArmeriaSpringActuatorAutoConfiguration::class.java,
+      EndpointAutoConfiguration::class.java,
+      PropertyPlaceholderAutoConfiguration::class.java,
+      ZipkinServerConfigurationTest.Config::class.java,
+      ZipkinServerConfiguration::class.java,
+      TracingConfiguration::class.java
+    )
+    context.refresh()
+
+    context.getBean(Tracing::class.java).close()
+  }
+
+  @Test fun search_canDisable() {
+    TestPropertyValues.of("zipkin.storage.search-enabled:false").applyTo(context)
+    context.register(
+      ArmeriaSpringActuatorAutoConfiguration::class.java,
+      EndpointAutoConfiguration::class.java,
+      PropertyPlaceholderAutoConfiguration::class.java,
+      ZipkinServerConfigurationTest.Config::class.java,
+      ZipkinServerConfiguration::class.java
+    )
+    context.refresh()
+
+    val v2Storage = context.getBean(StorageComponent::class.java)
+    assertThat(v2Storage)
+      .extracting("searchEnabled")
+      .containsExactly(false)
+  }
+
+  @Configuration
+  open class Config {
+    @Bean open fun healthAggregator(): HealthAggregator {
+      return OrderedHealthAggregator()
+    }
+
+    @Bean open fun registry(): MeterRegistry {
+      return PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
+    }
+  }
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/server/internal/brave/ITZipkinSelfTracing.kt b/zipkin-server/src/test/kotlin/zipkin2/server/internal/brave/ITZipkinSelfTracing.kt
new file mode 100644
index 0000000..1258c8b
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/brave/ITZipkinSelfTracing.kt
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.server.internal.brave
+
+import com.linecorp.armeria.server.Server
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.test.context.junit4.SpringRunner
+import zipkin.server.ZipkinServer
+import zipkin2.server.internal.Http
+import zipkin2.storage.InMemoryStorage
+
+@SpringBootTest(
+  classes = [ZipkinServer::class],
+  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+  properties = ["spring.config.name=zipkin-server", "zipkin.self-tracing.enabled=true"]
+)
+@RunWith(SpringRunner::class)
+class ITZipkinSelfTracing {
+  @Autowired lateinit var server: Server
+  @Autowired lateinit var storage: TracingStorageComponent
+  @Before fun clearStorage() = (storage.delegate as InMemoryStorage).clear()
+
+  @Test fun getIsTraced_v2() {
+    assertThat(Http.getAsString(server, "/api/v2/services"))
+      .isEqualTo("[]")
+
+    assertServerTraced()
+  }
+
+  @Test fun postIsTraced_v1() {
+    assertThat(Http.post(server, "/api/v1/spans", body = "[]").isSuccessful).isTrue()
+
+    assertServerTraced()
+  }
+
+  @Test fun postIsTraced_v2() {
+    assertThat(Http.post(server, "/api/v2/spans", body = "[]").isSuccessful).isTrue()
+
+    assertServerTraced()
+  }
+
+  private fun assertServerTraced() {
+    Thread.sleep(1500) // wait for reporting interval
+
+    assertThat(Http.getAsString(server, "/api/v2/services"))
+      .isEqualTo("[\"zipkin-server\"]")
+  }
+}
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/mysql/Access.java b/zipkin-server/src/test/kotlin/zipkin2/server/internal/cassandra/Access.kt
similarity index 72%
copy from zipkin-server/src/test/java/zipkin2/server/internal/mysql/Access.java
copy to zipkin-server/src/test/kotlin/zipkin2/server/internal/cassandra/Access.kt
index 6f01643..a2b818f 100644
--- a/zipkin-server/src/test/java/zipkin2/server/internal/mysql/Access.java
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/cassandra/Access.kt
@@ -14,16 +14,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package zipkin2.server.internal.mysql;
+package zipkin2.server.internal.cassandra
 
-import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
 
-/** opens package access for testing */
-public final class Access {
-
-  public static void registerMySQL(AnnotationConfigApplicationContext context) {
-    context.register(
-        PropertyPlaceholderAutoConfiguration.class, ZipkinMySQLStorageConfiguration.class);
-  }
+/** opens package access for testing  */
+object Access {
+  fun registerCassandra(context: AnnotationConfigApplicationContext) = context.register(
+    PropertyPlaceholderAutoConfiguration::class.java,
+    ZipkinCassandraStorageConfiguration::class.java)
 }
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/cassandra/Access.java b/zipkin-server/src/test/kotlin/zipkin2/server/internal/cassandra3/Access.kt
similarity index 71%
rename from zipkin-server/src/test/java/zipkin2/server/internal/cassandra/Access.java
rename to zipkin-server/src/test/kotlin/zipkin2/server/internal/cassandra3/Access.kt
index ee6ecf1..a82942c 100644
--- a/zipkin-server/src/test/java/zipkin2/server/internal/cassandra/Access.java
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/cassandra3/Access.kt
@@ -14,16 +14,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package zipkin2.server.internal.cassandra;
+package zipkin2.server.internal.cassandra3
 
-import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
 
-/** opens package access for testing */
-public final class Access {
-
-  public static void registerCassandra(AnnotationConfigApplicationContext context) {
-    context.register(
-        PropertyPlaceholderAutoConfiguration.class, ZipkinCassandraStorageConfiguration.class);
-  }
+/** opens package access for testing  */
+object Access {
+  fun registerCassandra3(context: AnnotationConfigApplicationContext) = context.register(
+    PropertyPlaceholderAutoConfiguration::class.java,
+    ZipkinCassandra3StorageConfiguration::class.java)
 }
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/cassandra3/Access.java b/zipkin-server/src/test/kotlin/zipkin2/server/internal/elasticsearch/Access.kt
similarity index 71%
rename from zipkin-server/src/test/java/zipkin2/server/internal/cassandra3/Access.java
rename to zipkin-server/src/test/kotlin/zipkin2/server/internal/elasticsearch/Access.kt
index f6b2e26..49769e6 100644
--- a/zipkin-server/src/test/java/zipkin2/server/internal/cassandra3/Access.java
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/elasticsearch/Access.kt
@@ -14,16 +14,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package zipkin2.server.internal.cassandra3;
+package zipkin2.server.internal.elasticsearch
 
-import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
 
-/** opens package access for testing */
-public final class Access {
-
-  public static void registerCassandra3(AnnotationConfigApplicationContext context) {
-    context.register(
-        PropertyPlaceholderAutoConfiguration.class, ZipkinCassandra3StorageConfiguration.class);
-  }
+/** opens package access for testing  */
+object Access {
+  fun registerElasticsearchHttp(context: AnnotationConfigApplicationContext) = context.register(
+    PropertyPlaceholderAutoConfiguration::class.java,
+    ZipkinElasticsearchStorageAutoConfiguration::class.java)
 }
diff --git a/zipkin-server/src/test/kotlin/zipkin2/server/internal/elasticsearch/BasicAuthInterceptorTest.kt b/zipkin-server/src/test/kotlin/zipkin2/server/internal/elasticsearch/BasicAuthInterceptorTest.kt
new file mode 100644
index 0000000..40ec38c
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/elasticsearch/BasicAuthInterceptorTest.kt
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.server.internal.elasticsearch
+
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.ExpectedException
+
+class BasicAuthInterceptorTest {
+  @Rule @JvmField val mockWebServer = MockWebServer()
+  @Rule @JvmField val thrown: ExpectedException = ExpectedException.none()
+
+  var client: OkHttpClient = OkHttpClient.Builder()
+    .addNetworkInterceptor(BasicAuthInterceptor(ZipkinElasticsearchStorageProperties()))
+    .build()
+
+  @Test fun intercept_whenESReturns403AndJsonBody_throwsWithResponseBodyMessage() {
+    thrown.expect(IllegalStateException::class.java)
+    thrown.expectMessage("Sadness.")
+
+    mockWebServer.enqueue(
+      MockResponse().setResponseCode(403).setBody("{\"message\":\"Sadness.\"}"))
+
+    client.newCall(Request.Builder().url(mockWebServer.url("/")).build()).execute()
+  }
+}
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/kafka/Access.java b/zipkin-server/src/test/kotlin/zipkin2/server/internal/kafka/Access.kt
similarity index 56%
rename from zipkin-server/src/test/java/zipkin2/server/internal/kafka/Access.java
rename to zipkin-server/src/test/kotlin/zipkin2/server/internal/kafka/Access.kt
index b439268..cfd1d35 100644
--- a/zipkin-server/src/test/java/zipkin2/server/internal/kafka/Access.java
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/kafka/Access.kt
@@ -14,29 +14,25 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package zipkin2.server.internal.kafka;
+package zipkin2.server.internal.kafka
 
-import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
-import org.springframework.context.annotation.Configuration;
-import zipkin2.collector.kafka.KafkaCollector;
+import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
+import org.springframework.boot.context.properties.EnableConfigurationProperties
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
+import org.springframework.context.annotation.Configuration
+import zipkin2.collector.kafka.KafkaCollector
 
-/** opens package access for testing */
-public final class Access {
-
-  /** Just registering properties to avoid automatically connecting to a Kafka server */
-  public static void registerKafkaProperties(AnnotationConfigApplicationContext context) {
-    context.register(
-        PropertyPlaceholderAutoConfiguration.class, EnableKafkaCollectorProperties.class);
-  }
+/** opens package access for testing  */
+object Access {
+  /** Just registering properties to avoid automatically connecting to a Kafka server  */
+  fun registerKafkaProperties(context: AnnotationConfigApplicationContext) = context.register(
+    PropertyPlaceholderAutoConfiguration::class.java,
+    EnableKafkaCollectorProperties::class.java)
 
   @Configuration
-  @EnableConfigurationProperties(ZipkinKafkaCollectorProperties.class)
-  static class EnableKafkaCollectorProperties {}
+  @EnableConfigurationProperties(ZipkinKafkaCollectorProperties::class)
+  open class EnableKafkaCollectorProperties
 
-  public static KafkaCollector.Builder collectorBuilder(
-      AnnotationConfigApplicationContext context) {
-    return context.getBean(ZipkinKafkaCollectorProperties.class).toBuilder();
-  }
+  fun collectorBuilder(context: AnnotationConfigApplicationContext): KafkaCollector.Builder =
+    context.getBean(ZipkinKafkaCollectorProperties::class.java).toBuilder()
 }
diff --git a/zipkin-server/src/test/kotlin/zipkin2/server/internal/kafka/ZipkinKafkaCollectorConfigurationTest.kt b/zipkin-server/src/test/kotlin/zipkin2/server/internal/kafka/ZipkinKafkaCollectorConfigurationTest.kt
new file mode 100644
index 0000000..47de004
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/kafka/ZipkinKafkaCollectorConfigurationTest.kt
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.server.internal.kafka
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.After
+import org.junit.Test
+import org.springframework.beans.factory.NoSuchBeanDefinitionException
+import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
+import org.springframework.boot.test.util.TestPropertyValues
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
+import zipkin2.collector.kafka.KafkaCollector
+import zipkin2.server.internal.InMemoryCollectorConfiguration
+
+class ZipkinKafkaCollectorConfigurationTest {
+  val context = AnnotationConfigApplicationContext()
+  @After fun closeContext() = context.close()
+
+  @Test(expected = NoSuchBeanDefinitionException::class)
+  fun doesNotProvideCollectorComponent_whenBootstrapServersUnset() {
+    context.register(
+      PropertyPlaceholderAutoConfiguration::class.java,
+      ZipkinKafkaCollectorConfiguration::class.java,
+      InMemoryCollectorConfiguration::class.java)
+    context.refresh()
+
+    context.getBean(KafkaCollector::class.java)
+  }
+
+  @Test(expected = NoSuchBeanDefinitionException::class)
+  fun providesCollectorComponent_whenBootstrapServersEmptyString() {
+    TestPropertyValues.of("zipkin.collector.kafka.bootstrap-servers:").applyTo(context)
+    context.register(
+      PropertyPlaceholderAutoConfiguration::class.java,
+      ZipkinKafkaCollectorConfiguration::class.java,
+      InMemoryCollectorConfiguration::class.java)
+    context.refresh()
+
+    context.getBean(KafkaCollector::class.java)
+  }
+
+  @Test fun providesCollectorComponent_whenBootstrapServersSet() {
+    TestPropertyValues.of("zipkin.collector.kafka.bootstrap-servers:localhost:9091")
+      .applyTo(context)
+    context.register(
+      PropertyPlaceholderAutoConfiguration::class.java,
+      ZipkinKafkaCollectorConfiguration::class.java,
+      InMemoryCollectorConfiguration::class.java)
+    context.refresh()
+
+    assertThat(context.getBean(KafkaCollector::class.java)).isNotNull
+  }
+}
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/mysql/Access.java b/zipkin-server/src/test/kotlin/zipkin2/server/internal/kafka/ZipkinKafkaCollectorPropertiesTest.kt
similarity index 62%
rename from zipkin-server/src/test/java/zipkin2/server/internal/mysql/Access.java
rename to zipkin-server/src/test/kotlin/zipkin2/server/internal/kafka/ZipkinKafkaCollectorPropertiesTest.kt
index 6f01643..eacbacf 100644
--- a/zipkin-server/src/test/java/zipkin2/server/internal/mysql/Access.java
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/kafka/ZipkinKafkaCollectorPropertiesTest.kt
@@ -14,16 +14,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package zipkin2.server.internal.mysql;
+package zipkin2.server.internal.kafka
 
-import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
 
-/** opens package access for testing */
-public final class Access {
-
-  public static void registerMySQL(AnnotationConfigApplicationContext context) {
-    context.register(
-        PropertyPlaceholderAutoConfiguration.class, ZipkinMySQLStorageConfiguration.class);
+class ZipkinKafkaCollectorPropertiesTest {
+  @Test fun stringPropertiesConvertEmptyStringsToNull() {
+    val properties = ZipkinKafkaCollectorProperties()
+    properties.bootstrapServers = ""
+    properties.groupId = ""
+    properties.topic = ""
+    assertThat(properties.bootstrapServers).isNull()
+    assertThat(properties.groupId).isNull()
+    assertThat(properties.topic).isNull()
   }
 }
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/elasticsearch/Access.java b/zipkin-server/src/test/kotlin/zipkin2/server/internal/mysql/Access.kt
similarity index 70%
rename from zipkin-server/src/test/java/zipkin2/server/internal/elasticsearch/Access.java
rename to zipkin-server/src/test/kotlin/zipkin2/server/internal/mysql/Access.kt
index 25ac0f5..d703733 100644
--- a/zipkin-server/src/test/java/zipkin2/server/internal/elasticsearch/Access.java
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/mysql/Access.kt
@@ -14,17 +14,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package zipkin2.server.internal.elasticsearch;
+package zipkin2.server.internal.mysql
 
-import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
 
-/** opens package access for testing */
-public final class Access {
-
-  public static void registerElasticsearchHttp(AnnotationConfigApplicationContext context) {
-    context.register(
-      PropertyPlaceholderAutoConfiguration.class,
-      ZipkinElasticsearchStorageAutoConfiguration.class);
-  }
+/** opens package access for testing  */
+object Access {
+  fun registerMySQL(context: AnnotationConfigApplicationContext) = context.register(
+    PropertyPlaceholderAutoConfiguration::class.java,
+    ZipkinMySQLStorageConfiguration::class.java)
 }
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/prometheus/ZipkinPrometheusMetricsConfigurationTest.java b/zipkin-server/src/test/kotlin/zipkin2/server/internal/prometheus/ZipkinPrometheusMetricsConfigurationTest.kt
similarity index 50%
rename from zipkin-server/src/test/java/zipkin2/server/internal/prometheus/ZipkinPrometheusMetricsConfigurationTest.java
rename to zipkin-server/src/test/kotlin/zipkin2/server/internal/prometheus/ZipkinPrometheusMetricsConfigurationTest.kt
index 08dd021..1081041 100644
--- a/zipkin-server/src/test/java/zipkin2/server/internal/prometheus/ZipkinPrometheusMetricsConfigurationTest.java
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/prometheus/ZipkinPrometheusMetricsConfigurationTest.kt
@@ -14,54 +14,50 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package zipkin2.server.internal.prometheus;
-
-import com.linecorp.armeria.spring.ArmeriaServerConfigurator;
-import org.junit.After;
-import org.junit.Test;
-import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
-import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusMetricsExportAutoConfiguration;
-import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
-import org.springframework.boot.test.util.TestPropertyValues;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class ZipkinPrometheusMetricsConfigurationTest {
-  AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
-
-  public void refresh() {
-    context.register(
-      PropertyPlaceholderAutoConfiguration.class,
-      MetricsAutoConfiguration.class,
-      PrometheusMetricsExportAutoConfiguration.class,
-      ZipkinPrometheusMetricsConfiguration.class
-    );
-    context.refresh();
+package zipkin2.server.internal.prometheus
+
+import com.linecorp.armeria.spring.ArmeriaServerConfigurator
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.After
+import org.junit.Test
+import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration
+import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusMetricsExportAutoConfiguration
+import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
+import org.springframework.boot.test.util.TestPropertyValues
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
+
+class ZipkinPrometheusMetricsConfigurationTest {
+  val context = AnnotationConfigApplicationContext()
+  @After fun closeContext() = context.close()
+
+  @Test fun providesHttpRequestDurationCustomizer() {
+    refresh()
+
+    context.getBeansOfType(ArmeriaServerConfigurator::class.java)
   }
 
-  @After public void close() {
-    context.close();
-  }
-
-  @Test public void providesHttpRequestDurationCustomizer() {
-    refresh();
+  @Test fun defaultMetricName() {
+    refresh()
 
-    context.getBeansOfType(ArmeriaServerConfigurator.class);
+    assertThat(context.getBean(ZipkinPrometheusMetricsConfiguration::class.java).metricName)
+      .isEqualTo("http.server.requests")
   }
 
-  @Test public void defaultMetricName() {
-    refresh();
+  @Test fun overrideMetricName() {
+    TestPropertyValues.of("management.metrics.web.server.requests-metric-name:foo").applyTo(context)
+    refresh()
 
-    assertThat(context.getBean(ZipkinPrometheusMetricsConfiguration.class).metricName)
-      .isEqualTo("http.server.requests");
+    assertThat(context.getBean(ZipkinPrometheusMetricsConfiguration::class.java).metricName)
+      .isEqualTo("foo")
   }
 
-  @Test public void overrideMetricName() {
-    TestPropertyValues.of("management.metrics.web.server.requests-metric-name:foo").applyTo(context);
-    refresh();
-
-    assertThat(context.getBean(ZipkinPrometheusMetricsConfiguration.class).metricName)
-      .isEqualTo("foo");
+  fun refresh() {
+    context.register(
+      PropertyPlaceholderAutoConfiguration::class.java,
+      MetricsAutoConfiguration::class.java,
+      PrometheusMetricsExportAutoConfiguration::class.java,
+      ZipkinPrometheusMetricsConfiguration::class.java
+    )
+    context.refresh()
   }
 }
diff --git a/zipkin-server/src/test/java/zipkin2/server/internal/rabbitmq/Access.java b/zipkin-server/src/test/kotlin/zipkin2/server/internal/rabbitmq/Access.kt
similarity index 58%
rename from zipkin-server/src/test/java/zipkin2/server/internal/rabbitmq/Access.java
rename to zipkin-server/src/test/kotlin/zipkin2/server/internal/rabbitmq/Access.kt
index 729dd59..049f550 100644
--- a/zipkin-server/src/test/java/zipkin2/server/internal/rabbitmq/Access.java
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/rabbitmq/Access.kt
@@ -14,29 +14,25 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package zipkin2.server.internal.rabbitmq;
+package zipkin2.server.internal.rabbitmq
 
-import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
-import org.springframework.context.annotation.Configuration;
-import zipkin2.collector.rabbitmq.RabbitMQCollector;
+import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
+import org.springframework.boot.context.properties.EnableConfigurationProperties
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
+import org.springframework.context.annotation.Configuration
+import zipkin2.collector.rabbitmq.RabbitMQCollector
 
-/** opens package access for testing */
-public final class Access {
-
-  /** Just registering properties to avoid automatically connecting to a Rabbit MQ server */
-  public static void registerRabbitMQProperties(AnnotationConfigApplicationContext context) {
-    context.register(
-        PropertyPlaceholderAutoConfiguration.class, EnableRabbitMQCollectorProperties.class);
-  }
+/** opens package access for testing  */
+object Access {
+  /** Just registering properties to avoid automatically connecting to a Rabbit MQ server  */
+  fun registerRabbitMQProperties(context: AnnotationConfigApplicationContext) = context.register(
+    PropertyPlaceholderAutoConfiguration::class.java,
+    EnableRabbitMQCollectorProperties::class.java)
 
   @Configuration
-  @EnableConfigurationProperties(ZipkinRabbitMQCollectorProperties.class)
-  static class EnableRabbitMQCollectorProperties {}
+  @EnableConfigurationProperties(ZipkinRabbitMQCollectorProperties::class)
+  open class EnableRabbitMQCollectorProperties
 
-  public static RabbitMQCollector.Builder collectorBuilder(
-      AnnotationConfigApplicationContext context) throws Exception {
-    return context.getBean(ZipkinRabbitMQCollectorProperties.class).toBuilder();
-  }
+  fun collectorBuilder(context: AnnotationConfigApplicationContext): RabbitMQCollector.Builder =
+    context.getBean(ZipkinRabbitMQCollectorProperties::class.java).toBuilder()
 }
diff --git a/zipkin-server/src/test/kotlin/zipkin2/server/internal/rabbitmq/ZipkinRabbitMQCollectorConfigurationTest.kt b/zipkin-server/src/test/kotlin/zipkin2/server/internal/rabbitmq/ZipkinRabbitMQCollectorConfigurationTest.kt
new file mode 100644
index 0000000..92b104f
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/rabbitmq/ZipkinRabbitMQCollectorConfigurationTest.kt
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.server.internal.rabbitmq
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.After
+import org.junit.Ignore
+import org.junit.Test
+import org.springframework.beans.factory.NoSuchBeanDefinitionException
+import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
+import org.springframework.boot.test.util.TestPropertyValues
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
+import zipkin2.collector.rabbitmq.RabbitMQCollector
+import zipkin2.server.internal.InMemoryCollectorConfiguration
+
+class ZipkinRabbitMQCollectorConfigurationTest {
+  val context = AnnotationConfigApplicationContext()
+  @After fun closeContext() = context.close()
+
+  @Test(expected = NoSuchBeanDefinitionException::class)
+  fun doesNotProvideCollectorComponent_whenAddressAndUriNotSet() {
+    context.register(
+      PropertyPlaceholderAutoConfiguration::class.java,
+      ZipkinRabbitMQCollectorConfiguration::class.java,
+      InMemoryCollectorConfiguration::class.java)
+    context.refresh()
+
+    context.getBean(RabbitMQCollector::class.java)
+  }
+
+  @Test(expected = NoSuchBeanDefinitionException::class)
+  fun doesNotProvideCollectorComponent_whenAddressesAndUriIsEmptyString() {
+    TestPropertyValues.of(
+      "zipkin.collector.rabbitmq.addresses:",
+      "zipkin.collector.rabbitmq.uri:")
+      .applyTo(context)
+    context.register(
+      PropertyPlaceholderAutoConfiguration::class.java,
+      ZipkinRabbitMQCollectorConfiguration::class.java,
+      InMemoryCollectorConfiguration::class.java)
+    context.refresh()
+
+    context.getBean(RabbitMQCollector::class.java)
+  }
+
+  @Test @Ignore fun providesCollectorComponent_whenAddressesSet() {
+    TestPropertyValues.of("zipkin.collector.rabbitmq.addresses=localhost:5672").applyTo(context)
+    context.register(
+      PropertyPlaceholderAutoConfiguration::class.java,
+      ZipkinRabbitMQCollectorConfiguration::class.java,
+      InMemoryCollectorConfiguration::class.java)
+    context.refresh()
+
+    assertThat(context.getBean(RabbitMQCollector::class.java)).isNotNull
+  }
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/server/internal/rabbitmq/ZipkinRabbitMQCollectorPropertiesTest.kt b/zipkin-server/src/test/kotlin/zipkin2/server/internal/rabbitmq/ZipkinRabbitMQCollectorPropertiesTest.kt
new file mode 100644
index 0000000..5ca2760
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/rabbitmq/ZipkinRabbitMQCollectorPropertiesTest.kt
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.server.internal.rabbitmq
+
+import com.rabbitmq.client.ConnectionFactory
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import java.net.URI
+
+class ZipkinRabbitMQCollectorPropertiesTest {
+
+  @Test fun uriProperlyParsedAndIgnoresOtherProperties_whenUriSet() {
+    val properties = ZipkinRabbitMQCollectorProperties()
+    properties.uri = URI.create("amqp://admin:admin@localhost:5678/myv")
+    properties.addresses = listOf("will_not^work!")
+    properties.username = "bob"
+    properties.password = "letmein"
+    properties.virtualHost = "drwho"
+
+    assertThat(properties.toBuilder())
+      .extracting("connectionFactory")
+      .allSatisfy { `object` ->
+        val connFactory = `object` as ConnectionFactory
+        assertThat(connFactory.host).isEqualTo("localhost")
+        assertThat(connFactory.port).isEqualTo(5678)
+        assertThat(connFactory.username).isEqualTo("admin")
+        assertThat(connFactory.password).isEqualTo("admin")
+        assertThat(connFactory.virtualHost).isEqualTo("myv")
+      }
+  }
+
+  /** This prevents an empty RABBIT_URI variable from being mistaken as a real one  */
+  @Test fun ignoresEmptyURI() {
+    val properties = ZipkinRabbitMQCollectorProperties()
+    properties.uri = URI.create("")
+
+    assertThat(properties.uri).isNull()
+  }
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/server/internal/ui/ITZipkinUiConfiguration.kt b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ui/ITZipkinUiConfiguration.kt
new file mode 100644
index 0000000..179a233
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ui/ITZipkinUiConfiguration.kt
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.server.internal.ui
+
+import com.linecorp.armeria.client.HttpClient
+import com.linecorp.armeria.common.HttpHeaderNames
+import com.linecorp.armeria.common.HttpHeaders
+import com.linecorp.armeria.common.HttpMethod
+import com.linecorp.armeria.server.Server
+import okhttp3.Headers
+import okio.Okio
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.junit4.SpringRunner
+import zipkin2.server.internal.Http
+
+@RunWith(SpringRunner::class)
+@SpringBootTest(
+  classes = [ITZipkinUiConfiguration.TestServer::class],
+  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+  properties = [
+    "zipkin.ui.base-path=/foozipkin",
+    "server.compression.enabled=true",
+    "server.compression.min-response-size=128"
+  ]
+)
+class ITZipkinUiConfiguration {
+  @Autowired lateinit var server: Server
+
+  /** The zipkin-ui is a single-page app. This prevents reloading all resources on each click.  */
+  @Test fun setsMaxAgeOnUiResources() {
+    assertThat(Http.get(server, "/zipkin/config.json").header("Cache-Control"))
+      .isEqualTo("max-age=600")
+    assertThat(Http.get(server, "/zipkin/index.html").header("Cache-Control"))
+      .isEqualTo("max-age=60")
+    assertThat(Http.get(server, "/zipkin/test.txt").header("Cache-Control"))
+      .isEqualTo("max-age=31536000")
+  }
+
+  @Test fun redirectsIndex() {
+    val index = Http.getAsString(server, "/zipkin/index.html")
+    assertThat(Http.getAsString(server, "/zipkin/")).isEqualTo(index)
+
+    listOf("/zipkin", "/").forEach { path ->
+      val response = Http.get(server, path)
+      assertThat(response.code()).isEqualTo(302)
+      assertThat(response.header("location")).isEqualTo("/zipkin/")
+    }
+  }
+
+  /** Browsers honor conditional requests such as eTag. Let's make sure the server does  */
+  @Test fun conditionalRequests() {
+    listOf("/zipkin/config.json", "/zipkin/index.html", "/zipkin/test.txt").forEach { path ->
+      val etag = Http.get(server, path).header("etag")
+      assertThat(Http.get(server, path, Headers.of("If-None-Match", etag)).code())
+        .isEqualTo(304)
+      assertThat(Http.get(server, path, Headers.of("If-None-Match", "aargh")).code())
+        .isEqualTo(200)
+    }
+  }
+
+  /** Some assets are pretty big. ensure they use compression.  */
+  @Test fun supportsCompression() {
+    assertThat(getContentEncodingFromRequestThatAcceptsGzip("/zipkin/test.txt"))
+      .isNull() // too small to compress
+    assertThat(getContentEncodingFromRequestThatAcceptsGzip("/zipkin/config.json"))
+      .isEqualTo("gzip")
+  }
+
+  /**
+   * The test sets the property `zipkin.ui.base-path=/foozipkin`, which should reflect in
+   * index.html
+   */
+  @Test fun replacesBaseTag() {
+    assertThat(Http.getAsString(server, "/zipkin/index.html"))
+      .isEqualToIgnoringWhitespace(stringFromClasspath("zipkin-ui/index.html")
+        .replace("<base href=\"/\" />", "<base href=\"/foozipkin/\">"))
+  }
+
+  /** index.html is served separately. This tests other content is also loaded from the classpath.  */
+  @Test fun servesOtherContentFromClasspath() {
+    assertThat(Http.getAsString(server, "/zipkin/test.txt"))
+      .isEqualToIgnoringWhitespace(stringFromClasspath("zipkin-ui/test.txt"))
+  }
+
+  @EnableAutoConfiguration
+  @Import(ZipkinUiConfiguration::class)
+  class TestServer
+
+  private fun stringFromClasspath(path: String): String {
+    val url = javaClass.classLoader.getResource(path)
+    assertThat(url).isNotNull()
+
+    url!!.openStream()
+      .use { fromClasspath -> return Okio.buffer(Okio.source(fromClasspath)).readUtf8() }
+  }
+
+  private fun getContentEncodingFromRequestThatAcceptsGzip(path: String): String? {
+    // We typically use OkHttp in our tests, but that automatically unzips..
+    val response = HttpClient.of(Http.url(server, ""))
+      .execute(HttpHeaders.of(HttpMethod.GET, path).set(HttpHeaderNames.ACCEPT_ENCODING, "gzip"))
+      .aggregate().join()
+    return response.headers().get(HttpHeaderNames.CONTENT_ENCODING)
+  }
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/server/internal/ui/ZipkinUiConfigurationTest.kt b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ui/ZipkinUiConfigurationTest.kt
new file mode 100644
index 0000000..3e270ac
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/server/internal/ui/ZipkinUiConfigurationTest.kt
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.server.internal.ui
+
+import com.linecorp.armeria.common.AggregatedHttpMessage
+import com.linecorp.armeria.common.HttpHeaderNames
+import com.linecorp.armeria.common.HttpHeaders
+import com.linecorp.armeria.common.HttpMethod
+import com.linecorp.armeria.common.HttpRequest
+import com.linecorp.armeria.common.MediaType
+import com.linecorp.armeria.server.ServiceRequestContext
+import io.netty.handler.codec.http.cookie.ClientCookieEncoder
+import io.netty.handler.codec.http.cookie.Cookie
+import io.netty.handler.codec.http.cookie.DefaultCookie
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.After
+import org.junit.Test
+import org.springframework.beans.factory.BeanCreationException
+import org.springframework.beans.factory.NoSuchBeanDefinitionException
+import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
+import org.springframework.boot.test.util.TestPropertyValues
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
+import org.springframework.core.io.ClassPathResource
+import java.io.ByteArrayInputStream
+import java.io.FileNotFoundException
+
+class ZipkinUiConfigurationTest {
+  val context = AnnotationConfigApplicationContext()
+  @After fun closeContext() = context.close()
+
+  @Test fun indexHtmlFromClasspath() {
+    refreshContext();
+
+    assertThat(context.getBean(ZipkinUiConfiguration::class.java).indexHtml)
+      .isNotNull
+  }
+
+  @Test fun indexContentType() {
+    refreshContext()
+
+    assertThat(serveIndex().headers().contentType())
+      .isEqualTo(MediaType.HTML_UTF_8)
+  }
+
+  @Test fun invalidIndexHtml() {
+    TestPropertyValues.of("zipkin.ui.basepath:/foo/bar").applyTo(context)
+    refreshContext()
+
+    // I failed to make Jsoup barf, even on nonsense like: "<head wait no I changed my mind this HTML is totally invalid <<<<<<<<<<<"
+    // So let's just run with a case where the file doesn't exist
+    val ui = context.getBean(ZipkinUiConfiguration::class.java)
+    ui.indexHtml = ClassPathResource("does-not-exist.html")
+
+    try {
+      serveIndex()
+
+      assertThat(false).isTrue()
+    } catch (e: RuntimeException) {
+      assertThat(e).hasRootCauseInstanceOf(FileNotFoundException::class.java)
+    }
+  }
+
+  @Test fun canOverridesProperty_defaultLookback() {
+    TestPropertyValues.of("zipkin.ui.defaultLookback:100").applyTo(context)
+    refreshContext()
+
+    assertThat(context.getBean(ZipkinUiProperties::class.java).defaultLookback)
+      .isEqualTo(100)
+  }
+
+  @Test fun canOverrideProperty_logsUrl() {
+    val url = "http://mycompany.com/kibana"
+    TestPropertyValues.of("zipkin.ui.logs-url:$url").applyTo(context)
+    refreshContext()
+
+    assertThat(context.getBean(ZipkinUiProperties::class.java).logsUrl).isEqualTo(url)
+  }
+
+  @Test fun logsUrlIsNullIfOverridenByEmpty() {
+    TestPropertyValues.of("zipkin.ui.logs-url:").applyTo(context)
+    refreshContext()
+
+    assertThat(context.getBean(ZipkinUiProperties::class.java).logsUrl).isNull()
+  }
+
+  @Test fun logsUrlIsNullByDefault() {
+    refreshContext()
+
+    assertThat(context.getBean(ZipkinUiProperties::class.java).logsUrl).isNull()
+  }
+
+  @Test(expected = NoSuchBeanDefinitionException::class)
+  fun canOverridesProperty_disable() {
+    TestPropertyValues.of("zipkin.ui.enabled:false").applyTo(context)
+    refreshContext()
+
+    context.getBean(ZipkinUiProperties::class.java)
+  }
+
+  @Test fun canOverridesProperty_searchEnabled() {
+    TestPropertyValues.of("zipkin.ui.search-enabled:false").applyTo(context)
+    refreshContext()
+
+    assertThat(context.getBean(ZipkinUiProperties::class.java).isSearchEnabled).isFalse()
+  }
+
+  @Test fun canOverrideProperty_dependencyLowErrorRate() {
+    TestPropertyValues.of("zipkin.ui.dependency.low-error-rate:0.1").applyTo(context)
+    refreshContext()
+
+    assertThat(context.getBean(ZipkinUiProperties::class.java).dependency.lowErrorRate)
+      .isEqualTo(0.1f)
+  }
+
+  @Test fun canOverrideProperty_dependencyHighErrorRate() {
+    TestPropertyValues.of("zipkin.ui.dependency.high-error-rate:0.1").applyTo(context)
+    refreshContext()
+
+    assertThat(context.getBean(ZipkinUiProperties::class.java).dependency.highErrorRate)
+      .isEqualTo(0.1f)
+  }
+
+  @Test fun defaultBaseUrl_doesNotChangeResource() {
+    refreshContext()
+
+    assertThat(ByteArrayInputStream(serveIndex().content().array()))
+      .hasSameContentAs(javaClass.getResourceAsStream("/zipkin-ui/index.html"))
+  }
+
+  @Test fun canOverideProperty_basePath() {
+    TestPropertyValues.of("zipkin.ui.basepath:/foo/bar").applyTo(context)
+    refreshContext()
+
+    assertThat(serveIndex().contentUtf8())
+      .contains("<base href=\"/foo/bar/\">")
+  }
+
+  @Test fun lensCookieOverridesIndex() {
+    refreshContext()
+
+    assertThat(serveIndex(DefaultCookie("lens", "true")).contentUtf8())
+      .contains("zipkin-lens")
+  }
+
+  @Test fun canOverideProperty_specialCaseRoot() {
+    TestPropertyValues.of("zipkin.ui.basepath:/").applyTo(context)
+    refreshContext()
+
+    assertThat(serveIndex().contentUtf8())
+      .contains("<base href=\"/\">")
+  }
+
+  private fun serveIndex(vararg cookies: Cookie): AggregatedHttpMessage {
+    val headers = HttpHeaders.of(HttpMethod.GET, "/")
+    val encodedCookies = ClientCookieEncoder.LAX.encode(*cookies)
+    if (encodedCookies != null) {
+      headers.set(HttpHeaderNames.COOKIE, encodedCookies)
+    }
+    val req = HttpRequest.of(headers)
+    return context.getBean(ZipkinUiConfiguration::class.java).indexSwitchingService()
+      .serve(ServiceRequestContext.of(req), req).aggregate()
+      .get()
+  }
+
+  private fun refreshContext() {
+    context.register(
+      PropertyPlaceholderAutoConfiguration::class.java,
+      ZipkinUiConfiguration::class.java)
+    context.refresh()
+  }
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/storage/cassandra/ZipkinCassandraStorageAutoConfigurationTest.kt b/zipkin-server/src/test/kotlin/zipkin2/storage/cassandra/ZipkinCassandraStorageAutoConfigurationTest.kt
new file mode 100644
index 0000000..ae0e1fd
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/storage/cassandra/ZipkinCassandraStorageAutoConfigurationTest.kt
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.storage.cassandra
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.After
+import org.junit.Test
+import org.springframework.beans.factory.NoSuchBeanDefinitionException
+import org.springframework.boot.test.util.TestPropertyValues
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
+import zipkin2.server.internal.cassandra3.Access
+
+class ZipkinCassandraStorageAutoConfigurationTest {
+  val context = AnnotationConfigApplicationContext()
+  @After fun closeContext() = context.close()
+
+  @Test(expected = NoSuchBeanDefinitionException::class)
+  fun doesntProvidesStorageComponent_whenStorageTypeNotCassandra() {
+    TestPropertyValues.of("zipkin.storage.type:elasticsearch").applyTo(context)
+    Access.registerCassandra3(context)
+    context.refresh()
+
+    context.getBean(CassandraStorage::class.java)
+  }
+
+  @Test fun providesStorageComponent_whenStorageTypeCassandra() {
+    TestPropertyValues.of("zipkin.storage.type:cassandra3").applyTo(context)
+    Access.registerCassandra3(context)
+    context.refresh()
+
+    assertThat(context.getBean(CassandraStorage::class.java)).isNotNull
+  }
+
+  @Test fun canOverridesProperty_contactPoints() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:cassandra3",
+      "zipkin.storage.cassandra3.contact-points:host1,host2" // note snake-case supported
+    ).applyTo(context)
+    Access.registerCassandra3(context)
+    context.refresh()
+
+    assertThat(context.getBean(CassandraStorage::class.java).contactPoints()).isEqualTo(
+      "host1,host2")
+  }
+
+  @Test fun strictTraceId_defaultsToTrue() {
+    TestPropertyValues.of("zipkin.storage.type:cassandra3").applyTo(context)
+    Access.registerCassandra3(context)
+    context.refresh()
+
+    assertThat(context.getBean(CassandraStorage::class.java).strictTraceId()).isTrue()
+  }
+
+  @Test fun strictTraceId_canSetToFalse() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:cassandra3",
+      "zipkin.storage.strict-trace-id:false")
+      .applyTo(context)
+    Access.registerCassandra3(context)
+    context.refresh()
+
+    assertThat(context.getBean(CassandraStorage::class.java).strictTraceId()).isFalse()
+  }
+
+  @Test fun searchEnabled_canSetToFalse() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:cassandra3",
+      "zipkin.storage.search-enabled:false")
+      .applyTo(context)
+    Access.registerCassandra3(context)
+    context.refresh()
+
+    assertThat(context.getBean(CassandraStorage::class.java).searchEnabled()).isFalse()
+  }
+
+  @Test fun autocompleteKeys_list() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:cassandra3",
+      "zipkin.storage.autocomplete-keys:environment")
+      .applyTo(context)
+    Access.registerCassandra3(context)
+    context.refresh()
+
+    assertThat(context.getBean(CassandraStorage::class.java).autocompleteKeys())
+      .containsOnly("environment")
+  }
+
+  @Test fun autocompleteTtl() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:cassandra3",
+      "zipkin.storage.autocomplete-ttl:60000")
+      .applyTo(context)
+    Access.registerCassandra3(context)
+    context.refresh()
+
+    assertThat(context.getBean(CassandraStorage::class.java).autocompleteTtl())
+      .isEqualTo(60000)
+  }
+
+  @Test fun autocompleteCardinality() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:cassandra3",
+      "zipkin.storage.autocomplete-cardinality:5000")
+      .applyTo(context)
+    Access.registerCassandra3(context)
+    context.refresh()
+
+    assertThat(context.getBean(CassandraStorage::class.java).autocompleteCardinality())
+      .isEqualTo(5000)
+  }
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/storage/cassandra/v1/ZipkinCassandraStorageConfigurationTest.kt b/zipkin-server/src/test/kotlin/zipkin2/storage/cassandra/v1/ZipkinCassandraStorageConfigurationTest.kt
new file mode 100644
index 0000000..a3ab8d8
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/storage/cassandra/v1/ZipkinCassandraStorageConfigurationTest.kt
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.storage.cassandra.v1
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.After
+import org.junit.Test
+import org.springframework.beans.factory.NoSuchBeanDefinitionException
+import org.springframework.boot.test.util.TestPropertyValues
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
+import zipkin2.server.internal.cassandra.Access
+
+class ZipkinCassandraStorageConfigurationTest {
+  val context = AnnotationConfigApplicationContext()
+  @After fun closeContext() = context.close()
+
+  @Test(expected = NoSuchBeanDefinitionException::class)
+  fun doesntProvidesStorageComponent_whenStorageTypeNotCassandra() {
+    TestPropertyValues.of("zipkin.storage.type:elasticsearch").applyTo(context)
+    Access.registerCassandra(context)
+    context.refresh()
+
+    context.getBean(CassandraStorage::class.java)
+  }
+
+  @Test fun providesStorageComponent_whenStorageTypeCassandra() {
+    TestPropertyValues.of("zipkin.storage.type:cassandra").applyTo(context)
+    Access.registerCassandra(context)
+    context.refresh()
+
+    assertThat(context.getBean(CassandraStorage::class.java)).isNotNull
+  }
+
+  @Test fun canOverridesProperty_contactPoints() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:cassandra",
+      "zipkin.storage.cassandra.contact-points:host1,host2" // note snake-case supported
+    ).applyTo(context)
+    Access.registerCassandra(context)
+    context.refresh()
+
+    assertThat(context.getBean(CassandraStorage::class.java).contactPoints).isEqualTo(
+      "host1,host2")
+  }
+
+  @Test fun strictTraceId_defaultsToTrue() {
+    TestPropertyValues.of("zipkin.storage.type:cassandra").applyTo(context)
+    Access.registerCassandra(context)
+    context.refresh()
+
+    assertThat(context.getBean(CassandraStorage::class.java).strictTraceId).isTrue()
+  }
+
+  @Test fun strictTraceId_canSetToFalse() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:cassandra",
+      "zipkin.storage.strict-trace-id:false")
+      .applyTo(context)
+    Access.registerCassandra(context)
+    context.refresh()
+
+    assertThat(context.getBean(CassandraStorage::class.java).strictTraceId).isFalse()
+  }
+
+  @Test fun autocompleteKeys_list() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:cassandra",
+      "zipkin.storage.autocomplete-keys:environment")
+      .applyTo(context)
+    Access.registerCassandra(context)
+    context.refresh()
+
+    assertThat(context.getBean(CassandraStorage::class.java).autocompleteKeys)
+      .containsOnly("environment")
+  }
+
+  @Test fun autocompleteTtl() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:cassandra",
+      "zipkin.storage.autocomplete-ttl:60000")
+      .applyTo(context)
+    Access.registerCassandra(context)
+    context.refresh()
+
+    assertThat(context.getBean(CassandraStorage::class.java).autocompleteTtl)
+      .isEqualTo(60000)
+  }
+
+  @Test fun autocompleteCardinality() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:cassandra",
+      "zipkin.storage.autocomplete-cardinality:5000")
+      .applyTo(context)
+    Access.registerCassandra(context)
+    context.refresh()
+
+    assertThat(context.getBean(CassandraStorage::class.java).autocompleteCardinality)
+      .isEqualTo(5000)
+  }
+}
diff --git a/zipkin-server/src/test/kotlin/zipkin2/storage/mysql/v1/ZipkinMySQLStorageConfigurationTest.kt b/zipkin-server/src/test/kotlin/zipkin2/storage/mysql/v1/ZipkinMySQLStorageConfigurationTest.kt
new file mode 100644
index 0000000..e104011
--- /dev/null
+++ b/zipkin-server/src/test/kotlin/zipkin2/storage/mysql/v1/ZipkinMySQLStorageConfigurationTest.kt
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 zipkin2.storage.mysql.v1
+
+import com.zaxxer.hikari.HikariDataSource
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.After
+import org.junit.Test
+import org.springframework.beans.factory.NoSuchBeanDefinitionException
+import org.springframework.boot.test.util.TestPropertyValues
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
+import zipkin2.server.internal.mysql.Access
+
+class ZipkinMySQLStorageConfigurationTest {
+  val context = AnnotationConfigApplicationContext()
+  @After fun closeContext() = context.close()
+
+  @Test(expected = NoSuchBeanDefinitionException::class)
+  fun doesntProvidesStorageComponent_whenStorageTypeNotMySQL() {
+    TestPropertyValues.of("zipkin.storage.type:cassandra").applyTo(context)
+    Access.registerMySQL(context)
+    context.refresh()
+
+    context.getBean(MySQLStorage::class.java)
+  }
+
+  @Test fun providesStorageComponent_whenStorageTypeMySQL() {
+    TestPropertyValues.of("zipkin.storage.type:mysql").applyTo(context)
+    Access.registerMySQL(context)
+    context.refresh()
+
+    assertThat(context.getBean(MySQLStorage::class.java)).isNotNull
+  }
+
+  @Test fun canOverridesProperty_username() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:mysql",
+      "zipkin.storage.mysql.username:robot")
+      .applyTo(context)
+    Access.registerMySQL(context)
+    context.refresh()
+
+    assertThat(context.getBean(HikariDataSource::class.java).username).isEqualTo("robot")
+  }
+
+  @Test fun strictTraceId_defaultsToTrue() {
+    TestPropertyValues.of("zipkin.storage.type:mysql").applyTo(context)
+    Access.registerMySQL(context)
+    context.refresh()
+
+    assertThat(context.getBean(MySQLStorage::class.java).strictTraceId).isTrue()
+  }
+
+  @Test fun strictTraceId_canSetToFalse() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:mysql",
+      "zipkin.storage.strict-trace-id:false")
+      .applyTo(context)
+    Access.registerMySQL(context)
+    context.refresh()
+
+    assertThat(context.getBean(MySQLStorage::class.java).strictTraceId).isFalse()
+  }
+
+  @Test fun searchEnabled_canSetToFalse() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:mysql",
+      "zipkin.storage.search-enabled:false")
+      .applyTo(context)
+    Access.registerMySQL(context)
+    context.refresh()
+
+    assertThat(context.getBean(MySQLStorage::class.java).searchEnabled).isFalse()
+  }
+
+  @Test fun autocompleteKeys_list() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:mysql",
+      "zipkin.storage.autocomplete-keys:environment")
+      .applyTo(context)
+    Access.registerMySQL(context)
+    context.refresh()
+
+    assertThat(context.getBean(MySQLStorage::class.java).autocompleteKeys)
+      .containsOnly("environment")
+  }
+
+  @Test fun usesJdbcUrl_whenPresent() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:mysql",
+      "zipkin.storage.mysql" + ".jdbc-url:jdbc:mysql://host1,host2,host3/zipkin")
+      .applyTo(context)
+    Access.registerMySQL(context)
+    context.refresh()
+
+    assertThat(context.getBean(HikariDataSource::class.java).jdbcUrl).isEqualTo(
+      "jdbc:mysql://host1,host2,host3/zipkin")
+  }
+
+  @Test fun usesRegularConfig_whenBlank() {
+    TestPropertyValues.of(
+      "zipkin.storage.type:mysql",
+      "zipkin.storage.mysql.jdbc-url:",
+      "zipkin.storage.mysql.host:host",
+      "zipkin.storage.mysql.port:3306",
+      "zipkin.storage.mysql.username:root",
+      "zipkin.storage.mysql.password:secret",
+      "zipkin.storage.mysql.db:zipkin")
+      .applyTo(context)
+    Access.registerMySQL(context)
+    context.refresh()
+
+    assertThat(context.getBean(HikariDataSource::class.java).jdbcUrl).isEqualTo(
+      "jdbc:mysql://host:3306/zipkin?autoReconnect=true&useSSL=false&useUnicode=yes&characterEncoding=UTF-8")
+  }
+}