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:15 UTC

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

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")
+  }
+}