CASSANDRASC-55: Extract the in-jvm dtest template for use in other projects

This also required moving some additional classes to `common` so they
could be used in the new test-framework project.

Patch by Doug Rohrer; Reviewed by Dinesh Joshi, Francisco Guerrero, Yifan Cai for CASSANDRASC-55
diff --git a/.gitignore b/.gitignore
index fa89706..453c686 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,7 +77,7 @@
 *.logdir_IS_UNDEFINED
 
 # Sidecar version - generated by build.gradle
-src/main/resources/sidecar.version
+sidecar.version
 
 # Sidecar copyDist files copied to root directory
 agents
@@ -90,3 +90,5 @@
 
 dtest-jars
 scripts/dependency-reduced-pom.xml
+
+default-stylesheet.xsl
diff --git a/CHANGES.txt b/CHANGES.txt
index 3757c78..be2fd52 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
 1.0.0
 -----
+ * Extract the in-jvm dtest template for use in other projects (CASSANDRASC-55)
  * Fix relocation of native libraries for vertx-client-shaded (CASSANDRASC-67)
  * Enrich RetriesExhaustedException to have more information for better visibility (CASSANDRASC-65)
  * Fix failing unit tests in Apache CI (CASSANDRASC-66)
diff --git a/adapters/base/build.gradle b/adapters/base/build.gradle
index cf14e7c..1e26849 100644
--- a/adapters/base/build.gradle
+++ b/adapters/base/build.gradle
@@ -25,6 +25,7 @@
 }
 
 group 'org.apache.cassandra.sidecar'
+
 version project.version
 
 sourceCompatibility = 1.8
@@ -59,7 +60,7 @@
         maven(MavenPublication) {
             from components.java
             groupId project.group
-            artifactId "${archivesBaseName}"
+            artifactId "adapters-base"
             version System.getenv("CODE_VERSION") ?: "${version}"
         }
     }
diff --git a/build.gradle b/build.gradle
index ef324ca..a76ff66 100644
--- a/build.gradle
+++ b/build.gradle
@@ -18,8 +18,11 @@
  */
 
 
-import java.nio.file.Files
+import com.github.spotbugs.SpotBugsTask
+import org.nosphere.apache.rat.RatTask
+
 import java.nio.file.Paths
+import java.nio.file.Files
 
 buildscript {
     dependencies {
@@ -53,6 +56,8 @@
 ext.dtestJar = System.getenv("DTEST_JAR") ?: "dtest-5.0.jar" // trunk is currently 5.0.jar - update when trunk moves
 println("Using DTest jar: ${ext.dtestJar}")
 
+// Force checkstyle, rat, and spotBugs to run before test tasks for faster feedback
+def codeCheckTasks = task("codeCheckTasks")
 
 allprojects {
     apply plugin: 'jacoco'
@@ -72,11 +77,22 @@
         excludeFilter = file("${project.rootDir}/spotbugs-exclude.xml")
     }
 
-    tasks.withType(com.github.spotbugs.SpotBugsTask) {
+    tasks.withType(SpotBugsTask) {
         reports.xml.enabled = false
         reports.html.enabled = true
     }
 
+    codeCheckTasks.dependsOn(tasks.withType(Checkstyle))
+    codeCheckTasks.dependsOn(tasks.withType(RatTask))
+    codeCheckTasks.dependsOn(tasks.withType(SpotBugsTask))
+
+    tasks.withType(Test) {
+        shouldRunAfter(codeCheckTasks)
+        shouldRunAfter(tasks.withType(Checkstyle))
+        shouldRunAfter(tasks.withType(RatTask))
+        shouldRunAfter(tasks.withType(SpotBugsTask))
+    }
+
 }
 
 group 'org.apache.cassandra'
@@ -196,9 +212,9 @@
     testFixturesImplementation("io.vertx:vertx-web:${project.vertxVersion}") {
         exclude group: 'junit', module: 'junit'
     }
-
     integrationTestImplementation(files("dtest-jars/${dtestJar}"))
     integrationTestImplementation("org.apache.cassandra:dtest-api:0.0.15")
+    // Needed by the Cassandra dtest framework
     integrationTestImplementation("org.junit.vintage:junit-vintage-engine:${junitVersion}")
 }
 
@@ -206,7 +222,8 @@
     doFirst {
         // Store current Cassandra Sidecar build version in an embedded resource file;
         // the file is either created or overwritten, and ignored by Git source control
-        new File("$projectDir/src/main/resources/sidecar.version").text = version
+        Files.createDirectories(Paths.get("$projectDir/common/src/main/resources"))
+        new File("$projectDir/common/src/main/resources/sidecar.version").text = version
     }
 }
 
@@ -377,7 +394,6 @@
 }
 
 rat {
-
     doFirst {
         def excludeFilePath = Paths.get("${buildDir}/.rat-excludes.txt")
         def excludeLines = Files.readAllLines(excludeFilePath)
@@ -412,7 +428,7 @@
 
 // copyDist gets called on every build
 copyDist.dependsOn installDist, copyJolokia
-check.dependsOn checkstyleMain, checkstyleTest, integrationTest, jacocoTestReport
+check.dependsOn codeCheckTasks, integrationTest, jacocoTestReport
 build.dependsOn copyDist, copyJolokia, copyDocs
 run.dependsOn build
 
diff --git a/src/main/java/org/apache/cassandra/sidecar/cluster/InstancesConfig.java b/common/src/main/java/org/apache/cassandra/sidecar/cluster/InstancesConfig.java
similarity index 100%
rename from src/main/java/org/apache/cassandra/sidecar/cluster/InstancesConfig.java
rename to common/src/main/java/org/apache/cassandra/sidecar/cluster/InstancesConfig.java
diff --git a/src/main/java/org/apache/cassandra/sidecar/cluster/InstancesConfigImpl.java b/common/src/main/java/org/apache/cassandra/sidecar/cluster/InstancesConfigImpl.java
similarity index 72%
rename from src/main/java/org/apache/cassandra/sidecar/cluster/InstancesConfigImpl.java
rename to common/src/main/java/org/apache/cassandra/sidecar/cluster/InstancesConfigImpl.java
index 0fcda22..2507795 100644
--- a/src/main/java/org/apache/cassandra/sidecar/cluster/InstancesConfigImpl.java
+++ b/common/src/main/java/org/apache/cassandra/sidecar/cluster/InstancesConfigImpl.java
@@ -18,6 +18,7 @@
 
 package org.apache.cassandra.sidecar.cluster;
 
+import java.net.UnknownHostException;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -26,6 +27,7 @@
 import java.util.stream.Collectors;
 
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.dns.DnsResolver;
 
 /**
  * Local implementation of InstancesConfig.
@@ -35,13 +37,14 @@
     private final Map<Integer, InstanceMetadata> idToInstanceMetadata;
     private final Map<String, InstanceMetadata> hostToInstanceMetadata;
     private final List<InstanceMetadata> instanceMetadataList;
+    private final DnsResolver dnsResolver;
 
-    public InstancesConfigImpl(InstanceMetadata instanceMetadata)
+    public InstancesConfigImpl(InstanceMetadata instanceMetadata, DnsResolver dnsResolver)
     {
-        this(Collections.singletonList(instanceMetadata));
+        this(Collections.singletonList(instanceMetadata), dnsResolver);
     }
 
-    public InstancesConfigImpl(List<InstanceMetadata> instanceMetadataList)
+    public InstancesConfigImpl(List<InstanceMetadata> instanceMetadataList, DnsResolver dnsResolver)
     {
         this.idToInstanceMetadata = instanceMetadataList.stream()
                                                         .collect(Collectors.toMap(InstanceMetadata::id,
@@ -50,6 +53,7 @@
                                                           .collect(Collectors.toMap(InstanceMetadata::host,
                                                                                     Function.identity()));
         this.instanceMetadataList = instanceMetadataList;
+        this.dnsResolver = dnsResolver;
     }
 
     @Override
@@ -75,7 +79,23 @@
         InstanceMetadata instanceMetadata = hostToInstanceMetadata.get(host);
         if (instanceMetadata == null)
         {
-            throw new NoSuchElementException("Instance with host address " + host + " not found");
+            try
+            {
+                instanceMetadata = hostToInstanceMetadata.get(dnsResolver.resolve(host));
+            }
+            catch (UnknownHostException e)
+            {
+                NoSuchElementException error = new NoSuchElementException("Instance with host address "
+                                                      + host +
+                                                      " not found, and an error occurred when " +
+                                                      "attempting to resolve its IP address.");
+                error.initCause(e);
+                throw error;
+            }
+            if (instanceMetadata == null)
+            {
+                throw new NoSuchElementException("Instance with host address " + host + " not found");
+            }
         }
         return instanceMetadata;
     }
diff --git a/src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadata.java b/common/src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadata.java
similarity index 100%
rename from src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadata.java
rename to common/src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadata.java
diff --git a/src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadataImpl.java b/common/src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadataImpl.java
similarity index 100%
rename from src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadataImpl.java
rename to common/src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadataImpl.java
diff --git a/src/main/java/org/apache/cassandra/sidecar/utils/IOUtils.java b/common/src/main/java/org/apache/cassandra/sidecar/common/utils/IOUtils.java
similarity index 97%
rename from src/main/java/org/apache/cassandra/sidecar/utils/IOUtils.java
rename to common/src/main/java/org/apache/cassandra/sidecar/common/utils/IOUtils.java
index 71e246b..b0ce935 100644
--- a/src/main/java/org/apache/cassandra/sidecar/utils/IOUtils.java
+++ b/common/src/main/java/org/apache/cassandra/sidecar/common/utils/IOUtils.java
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.sidecar.utils;
+package org.apache.cassandra.sidecar.common.utils;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
diff --git a/src/main/java/org/apache/cassandra/sidecar/utils/SidecarVersionProvider.java b/common/src/main/java/org/apache/cassandra/sidecar/common/utils/SidecarVersionProvider.java
similarity index 95%
rename from src/main/java/org/apache/cassandra/sidecar/utils/SidecarVersionProvider.java
rename to common/src/main/java/org/apache/cassandra/sidecar/common/utils/SidecarVersionProvider.java
index c9c4b34..8ee28e7 100644
--- a/src/main/java/org/apache/cassandra/sidecar/utils/SidecarVersionProvider.java
+++ b/common/src/main/java/org/apache/cassandra/sidecar/common/utils/SidecarVersionProvider.java
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.sidecar.utils;
+package org.apache.cassandra.sidecar.common.utils;
 
 /**
  * Retrieves, caches, and provides build version of this Sidecar binary
diff --git a/scripts/build-shaded-dtest-jar-local.sh b/scripts/build-shaded-dtest-jar-local.sh
index 8ab2791..9d00ff0 100755
--- a/scripts/build-shaded-dtest-jar-local.sh
+++ b/scripts/build-shaded-dtest-jar-local.sh
@@ -31,7 +31,7 @@
 echo "${GIT_HASH}"
 echo "${DTEST_ARTIFACT_ID}"
 
-ant clean
+ant realclean
 ant dtest-jar -Dno-checkstyle=true
 
 # Install the version that will be shaded
diff --git a/scripts/relocate-dtest-dependencies.pom b/scripts/relocate-dtest-dependencies.pom
index f7758f3..9c48bb7 100644
--- a/scripts/relocate-dtest-dependencies.pom
+++ b/scripts/relocate-dtest-dependencies.pom
@@ -32,7 +32,6 @@
     <properties>
         <project.type>library</project.type>
         <java.version>1.8</java.version>
-        <test.source.directory>src/test/unit/java</test.source.directory>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
 
@@ -71,9 +70,25 @@
     </dependencies>
 
     <build>
-        <testSourceDirectory>${test.source.directory}</testSourceDirectory>
 
         <plugins>
+            <!-- Skip main compilation as we only want to package existing dependencies -->
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>default-compile</id>
+                        <phase>compile</phase>
+                        <goals>
+                            <goal>compile</goal>
+                        </goals>
+                        <configuration>
+                            <skipMain>true</skipMain>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
             <!-- generate a shaded JAR -->
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml
index 9394a11..4134346 100644
--- a/spotbugs-exclude.xml
+++ b/spotbugs-exclude.xml
@@ -48,7 +48,7 @@
 
     <!-- Ignore GC call in CassandraTestTemplate as we want to GC after cluster shutdown -->
     <Match>
-        <Class name="org.apache.cassandra.sidecar.common.testing.CassandraTestTemplate" />
+        <Class name="org.apache.cassandra.sidecar.testing.CassandraTestTemplate" />
         <Bug pattern="DM_GC" />
     </Match>
 
diff --git a/src/main/java/org/apache/cassandra/sidecar/MainModule.java b/src/main/java/org/apache/cassandra/sidecar/MainModule.java
index 024db71..97b6554 100644
--- a/src/main/java/org/apache/cassandra/sidecar/MainModule.java
+++ b/src/main/java/org/apache/cassandra/sidecar/MainModule.java
@@ -48,6 +48,7 @@
 import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
 import org.apache.cassandra.sidecar.common.CassandraVersionProvider;
 import org.apache.cassandra.sidecar.common.dns.DnsResolver;
+import org.apache.cassandra.sidecar.common.utils.SidecarVersionProvider;
 import org.apache.cassandra.sidecar.common.utils.ValidationConfiguration;
 import org.apache.cassandra.sidecar.logging.SidecarLoggerHandler;
 import org.apache.cassandra.sidecar.routes.CassandraHealthHandler;
@@ -65,7 +66,6 @@
 import org.apache.cassandra.sidecar.routes.sstableuploads.SSTableUploadHandler;
 import org.apache.cassandra.sidecar.utils.ChecksumVerifier;
 import org.apache.cassandra.sidecar.utils.MD5ChecksumVerifier;
-import org.apache.cassandra.sidecar.utils.SidecarVersionProvider;
 import org.apache.cassandra.sidecar.utils.TimeProvider;
 
 /**
@@ -222,10 +222,14 @@
     @Provides
     @Singleton
     public Configuration configuration(CassandraVersionProvider cassandraVersionProvider,
-                                       SidecarVersionProvider sidecarVersionProvider) throws IOException
+                                       SidecarVersionProvider sidecarVersionProvider,
+                                       DnsResolver dnsResolver) throws IOException
     {
         final String confPath = System.getProperty("sidecar.config", "file://./conf/config.yaml");
-        return YAMLSidecarConfiguration.of(confPath, cassandraVersionProvider, sidecarVersionProvider.sidecarVersion());
+        return YAMLSidecarConfiguration.of(confPath,
+                                           cassandraVersionProvider,
+                                           sidecarVersionProvider.sidecarVersion(),
+                                           dnsResolver);
     }
 
     @Provides
diff --git a/src/main/java/org/apache/cassandra/sidecar/YAMLSidecarConfiguration.java b/src/main/java/org/apache/cassandra/sidecar/YAMLSidecarConfiguration.java
index c0aecb8..1723723 100644
--- a/src/main/java/org/apache/cassandra/sidecar/YAMLSidecarConfiguration.java
+++ b/src/main/java/org/apache/cassandra/sidecar/YAMLSidecarConfiguration.java
@@ -45,6 +45,7 @@
 import org.apache.cassandra.sidecar.common.CQLSessionProvider;
 import org.apache.cassandra.sidecar.common.CassandraVersionProvider;
 import org.apache.cassandra.sidecar.common.JmxClient;
+import org.apache.cassandra.sidecar.common.dns.DnsResolver;
 import org.apache.cassandra.sidecar.common.utils.ValidationConfiguration;
 import org.apache.cassandra.sidecar.common.utils.YAMLValidationConfiguration;
 import org.apache.cassandra.sidecar.config.CacheConfiguration;
@@ -155,12 +156,14 @@
      * @param confPath        the path to the Sidecar YAML configuration file
      * @param versionProvider a Cassandra version provider
      * @param sidecarVersion  the version of the Sidecar from the current binary
+     * @param dnsResolver     the DNS resolver to use
      * @return the {@link YAMLConfiguration} parsed from the YAML file
      * @throws IOException when reading the configuration from file fails
      */
     public static Configuration of(String confPath,
                                    CassandraVersionProvider versionProvider,
-                                   String sidecarVersion) throws IOException
+                                   String sidecarVersion,
+                                   DnsResolver dnsResolver) throws IOException
     {
         YAMLConfiguration yamlConf = yamlConfiguration(confPath);
         int healthCheckFrequencyMillis = yamlConf.getInt(HEALTH_CHECK_INTERVAL, 1000);
@@ -168,7 +171,7 @@
         InstancesConfig instancesConfig = instancesConfig(yamlConf,
                                                           versionProvider,
                                                           healthCheckFrequencyMillis,
-                                                          sidecarVersion);
+                                                          sidecarVersion, dnsResolver);
         CacheConfiguration ssTableImportCacheConfiguration = cacheConfig(yamlConf,
                                                                          SSTABLE_IMPORT_CACHE_CONFIGURATION,
                                                                          TimeUnit.HOURS.toMillis(2),
@@ -241,12 +244,14 @@
      * @param versionProvider            a Cassandra version provider
      * @param healthCheckFrequencyMillis the health check frequency configuration in milliseconds
      * @param sidecarVersion             the version of the Sidecar from the current binary
+     * @param dnsResolver                the DNS resolver to use when looking up host IP addresses by name
      * @return the parsed {@link InstancesConfig} from the {@code yamlConf} object
      */
     private static InstancesConfig instancesConfig(YAMLConfiguration yamlConf,
                                                    CassandraVersionProvider versionProvider,
                                                    int healthCheckFrequencyMillis,
-                                                   String sidecarVersion)
+                                                   String sidecarVersion,
+                                                   DnsResolver dnsResolver)
     {
         /* Since we are supporting handling multiple instances in Sidecar optionally, we prefer reading single instance
          * data over reading multiple instances section
@@ -258,7 +263,7 @@
                                                                       versionProvider,
                                                                       healthCheckFrequencyMillis,
                                                                       sidecarVersion);
-            return new InstancesConfigImpl(instanceMetadata);
+            return new InstancesConfigImpl(instanceMetadata, dnsResolver);
         }
 
         List<HierarchicalConfiguration<ImmutableNode>> instances = yamlConf.configurationsAt(CASSANDRA_INSTANCES);
@@ -271,7 +276,7 @@
                                                                       sidecarVersion);
             instanceMetas.add(instanceMetadata);
         }
-        return new InstancesConfigImpl(instanceMetas);
+        return new InstancesConfigImpl(instanceMetas, dnsResolver);
     }
 
     private static CacheConfiguration cacheConfig(YAMLConfiguration yamlConf, String prefix,
diff --git a/src/test/integration/org/apache/cassandra/sidecar/IntegrationTestBase.java b/src/test/integration/org/apache/cassandra/sidecar/IntegrationTestBase.java
index e13ac08..42c4a74 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/IntegrationTestBase.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/IntegrationTestBase.java
@@ -46,7 +46,9 @@
 import io.vertx.junit5.VertxTestContext;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
 import org.apache.cassandra.sidecar.common.data.QualifiedTableName;
-import org.apache.cassandra.sidecar.common.testing.CassandraTestContext;
+import org.apache.cassandra.sidecar.common.dns.DnsResolver;
+import org.apache.cassandra.sidecar.testing.CassandraSidecarTestContext;
+import org.apache.cassandra.testing.CassandraTestContext;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -65,12 +67,15 @@
     protected static final String TEST_KEYSPACE = "testkeyspace";
     private static final String TEST_TABLE_PREFIX = "testtable";
     private static final AtomicInteger TEST_TABLE_ID = new AtomicInteger(0);
+    protected CassandraSidecarTestContext sidecarTestContext;
 
     @BeforeEach
     void setup(CassandraTestContext cassandraTestContext) throws InterruptedException
     {
-        Injector injector = Guice.createInjector(Modules.override(new MainModule())
-                                                        .with(new IntegrationTestModule(cassandraTestContext)));
+        this.sidecarTestContext = CassandraSidecarTestContext.from(cassandraTestContext, DnsResolver.DEFAULT);
+        Injector injector = Guice.createInjector(Modules
+                                                 .override(new MainModule())
+                                                 .with(new IntegrationTestModule(this.sidecarTestContext)));
         server = injector.getInstance(HttpServer.class);
         vertx = injector.getInstance(Vertx.class);
         config = injector.getInstance(Configuration.class);
@@ -95,6 +100,7 @@
             logger.info("Close event received before timeout.");
         else
             logger.error("Close event timed out.");
+        sidecarTestContext.close();
     }
 
     protected void testWithClient(VertxTestContext context, Consumer<WebClient> tester) throws Exception
@@ -107,7 +113,7 @@
         assertThat(context.awaitCompletion(30, TimeUnit.SECONDS)).isTrue();
     }
 
-    protected void createTestKeyspace(CassandraTestContext cassandraTestContext)
+    protected void createTestKeyspace(CassandraSidecarTestContext cassandraTestContext)
     {
         Session session = maybeGetSession(cassandraTestContext);
 
@@ -118,7 +124,8 @@
         );
     }
 
-    protected QualifiedTableName createTestTable(CassandraTestContext cassandraTestContext, String createTableStatement)
+    protected QualifiedTableName createTestTable(CassandraSidecarTestContext cassandraTestContext,
+                                                 String createTableStatement)
     {
         Session session = maybeGetSession(cassandraTestContext);
         QualifiedTableName tableName = uniqueTestTableFullName();
@@ -126,7 +133,7 @@
         return tableName;
     }
 
-    protected Session maybeGetSession(CassandraTestContext cassandraTestContext)
+    protected Session maybeGetSession(CassandraSidecarTestContext cassandraTestContext)
     {
         Session session = cassandraTestContext.session();
         assertThat(session).isNotNull();
@@ -138,9 +145,9 @@
         return new QualifiedTableName(TEST_KEYSPACE, TEST_TABLE_PREFIX + TEST_TABLE_ID.getAndIncrement());
     }
 
-    public List<Path> findChildFile(CassandraTestContext context, String hostname, String target)
+    public List<Path> findChildFile(CassandraSidecarTestContext context, String hostname, String target)
     {
-        InstanceMetadata instanceConfig = context.getInstancesConfig().instanceFromHost("127.0.0.1");
+        InstanceMetadata instanceConfig = context.getInstancesConfig().instanceFromHost(hostname);
         List<String> parentDirectories = instanceConfig.dataDirs();
 
         return parentDirectories.stream().flatMap(s -> findChildFile(Paths.get(s), target).stream())
diff --git a/src/test/integration/org/apache/cassandra/sidecar/IntegrationTestModule.java b/src/test/integration/org/apache/cassandra/sidecar/IntegrationTestModule.java
index 2acf3c9..f936952 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/IntegrationTestModule.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/IntegrationTestModule.java
@@ -23,19 +23,19 @@
 import com.google.inject.Singleton;
 import org.apache.cassandra.sidecar.cluster.InstancesConfig;
 import org.apache.cassandra.sidecar.common.TestValidationConfiguration;
-import org.apache.cassandra.sidecar.common.testing.CassandraTestContext;
 import org.apache.cassandra.sidecar.common.utils.ValidationConfiguration;
 import org.apache.cassandra.sidecar.config.CacheConfiguration;
 import org.apache.cassandra.sidecar.config.WorkerPoolConfiguration;
+import org.apache.cassandra.sidecar.testing.CassandraSidecarTestContext;
 
 /**
  * Provides the basic dependencies for integration tests
  */
 public class IntegrationTestModule extends AbstractModule
 {
-    private final CassandraTestContext cassandraTestContext;
+    private final CassandraSidecarTestContext cassandraTestContext;
 
-    public IntegrationTestModule(CassandraTestContext cassandraTestContext)
+    public IntegrationTestModule(CassandraSidecarTestContext cassandraTestContext)
     {
         this.cassandraTestContext = cassandraTestContext;
     }
diff --git a/src/test/integration/org/apache/cassandra/sidecar/common/DelegateTest.java b/src/test/integration/org/apache/cassandra/sidecar/common/DelegateTest.java
index 03f165a..84cd605 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/common/DelegateTest.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/common/DelegateTest.java
@@ -22,59 +22,37 @@
 
 import com.datastax.driver.core.exceptions.TransportException;
 import org.apache.cassandra.distributed.api.NodeToolResult;
-import org.apache.cassandra.sidecar.adapters.base.CassandraFactory;
-import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
-import org.apache.cassandra.sidecar.common.dns.DnsResolver;
-import org.apache.cassandra.sidecar.common.testing.CassandraIntegrationTest;
-import org.apache.cassandra.sidecar.common.testing.CassandraTestContext;
-import org.apache.cassandra.sidecar.utils.SidecarVersionProvider;
+import org.apache.cassandra.sidecar.IntegrationTestBase;
+import org.apache.cassandra.testing.CassandraIntegrationTest;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
 /**
  * Ensures the Delegate works correctly
  */
-class DelegateTest
+class DelegateTest extends IntegrationTestBase
 {
-    private static CassandraAdapterDelegate getCassandraAdapterDelegate(CassandraTestContext context)
-    {
-        SidecarVersionProvider svp = new SidecarVersionProvider("/sidecar.version");
-        CassandraVersionProvider versionProvider = new CassandraVersionProvider.Builder()
-                                                       .add(new CassandraFactory(DnsResolver.DEFAULT,
-                                                                                 svp.sidecarVersion()))
-                                                       .build();
-        InstanceMetadata instanceMetadata = context.instancesConfig.instances().get(0);
-        CQLSessionProvider sessionProvider = new CQLSessionProvider(instanceMetadata.host(),
-                                                                    instanceMetadata.port(),
-                                                                    1000);
-        CassandraAdapterDelegate delegate = new CassandraAdapterDelegate(versionProvider,
-                                                                         sessionProvider,
-                                                                         context.jmxClient(),
-                                                                         svp.sidecarVersion());
-        return delegate;
-    }
-
     @CassandraIntegrationTest
-    void testCorrectVersionIsEnabled(CassandraTestContext context)
+    void testCorrectVersionIsEnabled()
     {
-        CassandraAdapterDelegate delegate = getCassandraAdapterDelegate(context);
+        CassandraAdapterDelegate delegate = sidecarTestContext.getInstancesConfig().instances().get(0).delegate();
         SimpleCassandraVersion version = delegate.version();
         assertThat(version).isNotNull();
-        assertThat(version.major).isEqualTo(context.version.major);
-        assertThat(version.minor).isEqualTo(context.version.minor);
-        assertThat(version).isGreaterThanOrEqualTo(context.version);
+        assertThat(version.major).isEqualTo(sidecarTestContext.version.major);
+        assertThat(version.minor).isEqualTo(sidecarTestContext.version.minor);
+        assertThat(version).isGreaterThanOrEqualTo(sidecarTestContext.version);
     }
 
     @CassandraIntegrationTest
-    void testHealthCheck(CassandraTestContext context) throws InterruptedException
+    void testHealthCheck() throws InterruptedException
     {
-        CassandraAdapterDelegate delegate = getCassandraAdapterDelegate(context);
+        CassandraAdapterDelegate delegate = sidecarTestContext.getInstancesConfig().instances().get(0).delegate();
 
         delegate.healthCheck();
 
         assertThat(delegate.isUp()).as("health check succeeds").isTrue();
 
-        NodeToolResult nodetoolResult = context.cluster.get(1).nodetoolResult("disablebinary");
+        NodeToolResult nodetoolResult = sidecarTestContext.cluster.get(1).nodetoolResult("disablebinary");
         assertThat(nodetoolResult.getRc())
         .withFailMessage("Failed to disable binary:\nstdout:" + nodetoolResult.getStdout()
                          + "\nstderr: " + nodetoolResult.getStderr())
@@ -94,7 +72,7 @@
         }
         assertThat(delegate.isUp()).as("health check fails after binary has been disabled").isFalse();
 
-        context.cluster.get(1).nodetool("enablebinary");
+        sidecarTestContext.cluster.get(1).nodetool("enablebinary");
 
         TimeUnit.SECONDS.sleep(1);
         delegate.healthCheck();
diff --git a/src/test/integration/org/apache/cassandra/sidecar/common/JmxClientTest.java b/src/test/integration/org/apache/cassandra/sidecar/common/JmxClientTest.java
index 846d1f8..7295e44 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/common/JmxClientTest.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/common/JmxClientTest.java
@@ -23,9 +23,9 @@
 
 import org.apache.cassandra.distributed.api.IInstanceConfig;
 import org.apache.cassandra.distributed.api.IUpgradeableInstance;
-import org.apache.cassandra.sidecar.common.testing.CassandraIntegrationTest;
-import org.apache.cassandra.sidecar.common.testing.CassandraTestContext;
 import org.apache.cassandra.sidecar.common.utils.GossipInfoParser;
+import org.apache.cassandra.testing.CassandraIntegrationTest;
+import org.apache.cassandra.testing.CassandraTestContext;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -46,7 +46,7 @@
             assertThat(opMode).isNotNull();
             assertThat(opMode).isIn("LEAVING", "JOINING", "NORMAL", "DECOMMISSIONED", "CLIENT");
 
-            IUpgradeableInstance instance = context.cluster.getFirstRunningInstance();
+            IUpgradeableInstance instance = context.getCluster().getFirstRunningInstance();
             IInstanceConfig config = instance.config();
             assertThat(jmxClient.host()).isEqualTo(config.broadcastAddress().getAddress().getHostAddress());
             assertThat(jmxClient.port()).isEqualTo(config.jmxPort());
@@ -101,7 +101,7 @@
 
     private static JmxClient createJmxClient(CassandraTestContext context)
     {
-        IUpgradeableInstance instance = context.cluster.getFirstRunningInstance();
+        IUpgradeableInstance instance = context.getCluster().getFirstRunningInstance();
         IInstanceConfig config = instance.config();
         return new JmxClient(config.broadcastAddress().getAddress().getHostAddress(), config.jmxPort());
     }
diff --git a/src/test/integration/org/apache/cassandra/sidecar/common/testing/CassandraTestTemplate.java b/src/test/integration/org/apache/cassandra/sidecar/common/testing/CassandraTestTemplate.java
deleted file mode 100644
index bac94b1..0000000
--- a/src/test/integration/org/apache/cassandra/sidecar/common/testing/CassandraTestTemplate.java
+++ /dev/null
@@ -1,220 +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 org.apache.cassandra.sidecar.common.testing;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.stream.Stream;
-
-import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
-import org.junit.jupiter.api.extension.BeforeEachCallback;
-import org.junit.jupiter.api.extension.Extension;
-import org.junit.jupiter.api.extension.ExtensionContext;
-import org.junit.jupiter.api.extension.ParameterContext;
-import org.junit.jupiter.api.extension.ParameterResolver;
-import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
-import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.vdurmont.semver4j.Semver;
-import org.apache.cassandra.distributed.UpgradeableCluster;
-import org.apache.cassandra.distributed.api.Feature;
-import org.apache.cassandra.distributed.api.TokenSupplier;
-import org.apache.cassandra.distributed.shared.Versions;
-import org.apache.cassandra.sidecar.adapters.base.CassandraFactory;
-import org.apache.cassandra.sidecar.common.CassandraVersionProvider;
-import org.apache.cassandra.sidecar.common.SimpleCassandraVersion;
-import org.apache.cassandra.sidecar.common.dns.DnsResolver;
-import org.apache.cassandra.sidecar.utils.SidecarVersionProvider;
-
-
-/**
- * Creates a test per version of Cassandra we are testing
- * Tests must be marked with {@link CassandraIntegrationTest}
- * <p>
- * This is a mix of parameterized tests + a custom extension.  we need to be able to provide the test context
- * to each test (like an extension) but also need to create multiple tests (like parameterized tests).  Unfortunately
- * the two don't play well with each other.  You can't get access to the parameters from the extension.
- * This test template allows us full control of the test lifecycle and lets us tightly couple the context to each test
- * we generate, since the same test can be run for multiple versions of C*.
- */
-public class CassandraTestTemplate implements TestTemplateInvocationContextProvider
-{
-
-    private static final Logger logger = LoggerFactory.getLogger(CassandraTestTemplate.class);
-    private static SidecarVersionProvider svp = new SidecarVersionProvider("/sidecar.version");
-
-    private UpgradeableCluster cluster;
-
-    @Override
-    public boolean supportsTestTemplate(ExtensionContext context)
-    {
-        return true;
-    }
-
-    @Override
-    public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context)
-    {
-        return new TestVersionSupplier().testVersions()
-                                        .map(v -> invocationContext(v, context));
-    }
-
-    /**
-     * Returns a {@link TestTemplateInvocationContext}
-     *
-     * @param version a version for the test
-     * @param context the <em>context</em> in which the current test or container is being executed.
-     * @return the <em>context</em> of a single invocation of a
-     * {@linkplain org.junit.jupiter.api.TestTemplate test template}
-     */
-    private TestTemplateInvocationContext invocationContext(TestVersion version, ExtensionContext context)
-    {
-        return new TestTemplateInvocationContext()
-        {
-            private CassandraTestContext cassandraTestContext;
-
-            /**
-             * A display name can be configured per test still - this adds the C* version we're testing automatically
-             * as a suffix to the name
-             * @param invocationIndex the index to the invocation
-             * @return the display name
-             */
-            @Override
-            public String getDisplayName(int invocationIndex)
-            {
-                return context.getDisplayName() + ": " + version.version();
-            }
-
-            /**
-             * Used to register the extensions required to start and stop the in-jvm dtest environment
-             * @return a list of registered {@link Extension extensions}
-             */
-            @Override
-            public List<Extension> getAdditionalExtensions()
-            {
-                return Arrays.asList(parameterResolver(), postProcessor(), beforeEach());
-            }
-
-            private BeforeEachCallback beforeEach()
-            {
-                return beforeEachCtx -> {
-                    CassandraIntegrationTest annotation =
-                    context.getElement().map(e -> e.getAnnotation(CassandraIntegrationTest.class)).get();
-                    // spin up a C* cluster using the in-jvm dtest
-                    Versions versions = Versions.find();
-                    int nodesPerDc = annotation.nodesPerDc();
-                    int dcCount = annotation.numDcs();
-                    int newNodesPerDc = annotation.newNodesPerDc(); // if the test wants to add more nodes later
-                    int finalNodeCount = dcCount * (nodesPerDc + newNodesPerDc);
-                    Versions.Version requestedVersion = versions.getLatest(new Semver(version.version(),
-                                                                                      Semver.SemverType.LOOSE));
-                    UpgradeableCluster.Builder builder =
-                    UpgradeableCluster.build(nodesPerDc)
-                                      .withVersion(requestedVersion)
-                                      .withDCs(dcCount)
-                                      .withDataDirCount(annotation.numDataDirsPerInstance())
-                                      .withConfig(config -> {
-                                          if (annotation.nativeTransport())
-                                          {
-                                              config.with(Feature.NATIVE_PROTOCOL);
-                                          }
-                                          if (annotation.jmx())
-                                          {
-                                              config.with(Feature.JMX);
-                                          }
-                                          if (annotation.gossip())
-                                          {
-                                              config.with(Feature.GOSSIP);
-                                          }
-                                          if (annotation.network())
-                                          {
-                                              config.with(Feature.NETWORK);
-                                          }
-                                      });
-                    TokenSupplier tokenSupplier = TokenSupplier.evenlyDistributedTokens(finalNodeCount,
-                                                                                        builder.getTokenCount());
-                    builder.withTokenSupplier(tokenSupplier);
-                    cluster = builder.start();
-
-                    logger.info("Testing {} against in-jvm dtest cluster", version);
-                    CassandraVersionProvider versionProvider = cassandraVersionProvider(DnsResolver.DEFAULT);
-                    SimpleCassandraVersion versionParsed = SimpleCassandraVersion.create(version.version());
-                    cassandraTestContext = new CassandraTestContext(versionParsed, cluster, versionProvider);
-                    logger.info("Created test context {}", cassandraTestContext);
-                };
-            }
-
-            /**
-             * Shuts down the in-jvm dtest cluster when the test is finished
-             * @return the {@link AfterTestExecutionCallback}
-             */
-            private AfterTestExecutionCallback postProcessor()
-            {
-                return postProcessorCtx -> {
-                    // Tear down the client-side before the cluster as we need to close some server-side connections
-                    // that can only be closed by clients?
-                    cassandraTestContext.close();
-                    // tear down the in-jvm cluster
-                    cluster.close();
-                };
-            }
-
-            /**
-             * Required for Junit to know the CassandraTestContext can be used in these tests
-             * @return a {@link ParameterResolver}
-             */
-            private ParameterResolver parameterResolver()
-            {
-                return new ParameterResolver()
-                {
-                    @Override
-                    public boolean supportsParameter(ParameterContext parameterContext,
-                                                     ExtensionContext extensionContext)
-                    {
-                        return parameterContext.getParameter().getType().equals(CassandraTestContext.class);
-                    }
-
-                    @Override
-                    public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
-                    {
-                        return cassandraTestContext;
-                    }
-                };
-            }
-        };
-    }
-
-    public CassandraVersionProvider cassandraVersionProvider(DnsResolver dnsResolver)
-    {
-        return new CassandraVersionProvider.Builder()
-               .add(new CassandraFactory(dnsResolver, svp.sidecarVersion())).build();
-    }
-
-    static
-    {
-        // Settings to reduce the test setup delay incurred if gossip is enabled
-        System.setProperty("cassandra.ring_delay_ms", "5000"); // down from 30s default
-        System.setProperty("cassandra.consistent.rangemovement", "false");
-        System.setProperty("cassandra.consistent.simultaneousmoves.allow", "true");
-        // End gossip delay settings
-        // Set the location of dtest jars
-        System.setProperty("cassandra.test.dtest_jar_path", "dtest-jars");
-    }
-}
diff --git a/src/test/integration/org/apache/cassandra/sidecar/routes/GossipInfoHandlerIntegrationTest.java b/src/test/integration/org/apache/cassandra/sidecar/routes/GossipInfoHandlerIntegrationTest.java
index 70285dc..4ad7a48 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/routes/GossipInfoHandlerIntegrationTest.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/routes/GossipInfoHandlerIntegrationTest.java
@@ -25,8 +25,8 @@
 import io.vertx.junit5.VertxTestContext;
 import org.apache.cassandra.sidecar.IntegrationTestBase;
 import org.apache.cassandra.sidecar.common.data.GossipInfoResponse;
-import org.apache.cassandra.sidecar.common.testing.CassandraIntegrationTest;
-import org.apache.cassandra.sidecar.common.testing.CassandraTestContext;
+import org.apache.cassandra.testing.CassandraIntegrationTest;
+import org.apache.cassandra.testing.CassandraTestContext;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -52,7 +52,7 @@
                       assertThat(gossipInfo.generation()).isNotNull();
                       assertThat(gossipInfo.heartbeat()).isNotNull();
                       assertThat(gossipInfo.hostId()).isNotNull();
-                      String releaseVersion = cassandraTestContext.cluster.getFirstRunningInstance()
+                      String releaseVersion = cassandraTestContext.getCluster().getFirstRunningInstance()
                                                                           .getReleaseVersionString();
                       assertThat(gossipInfo.releaseVersion()).startsWith(releaseVersion);
                       context.completeNow();
diff --git a/src/test/integration/org/apache/cassandra/sidecar/routes/RingHandlerIntegrationTest.java b/src/test/integration/org/apache/cassandra/sidecar/routes/RingHandlerIntegrationTest.java
index 01c5cc5..d2b06a2 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/routes/RingHandlerIntegrationTest.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/routes/RingHandlerIntegrationTest.java
@@ -34,8 +34,8 @@
 import org.apache.cassandra.sidecar.IntegrationTestBase;
 import org.apache.cassandra.sidecar.common.data.RingEntry;
 import org.apache.cassandra.sidecar.common.data.RingResponse;
-import org.apache.cassandra.sidecar.common.testing.CassandraIntegrationTest;
-import org.apache.cassandra.sidecar.common.testing.CassandraTestContext;
+import org.apache.cassandra.testing.CassandraIntegrationTest;
+import org.apache.cassandra.testing.CassandraTestContext;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -47,7 +47,7 @@
 {
 
     @CassandraIntegrationTest
-    void retrieveRingWithoutKeyspace(VertxTestContext context, CassandraTestContext cassandraTestContext)
+    void retrieveRingWithoutKeyspace(VertxTestContext context)
     throws Exception
     {
         String testRoute = "/api/v1/cassandra/ring";
@@ -55,7 +55,7 @@
             client.get(config.getPort(), "127.0.0.1", testRoute)
                   .expect(ResponsePredicate.SC_OK)
                   .send(context.succeeding(response -> {
-                      assertRingResponseOK(response, cassandraTestContext);
+                      assertRingResponseOK(response, sidecarTestContext);
                       context.completeNow();
                   }));
         });
@@ -76,12 +76,11 @@
     }
 
     @CassandraIntegrationTest
-    void retrieveRingWithExistingKeyspace(VertxTestContext context,
-                                          CassandraTestContext cassandraTestContext) throws Exception
+    void retrieveRingWithExistingKeyspace(VertxTestContext context) throws Exception
     {
-        createTestKeyspace(cassandraTestContext);
+        createTestKeyspace(sidecarTestContext);
         retrieveRingWithKeyspace(context, TEST_KEYSPACE, response -> {
-            assertRingResponseOK(response, cassandraTestContext);
+            assertRingResponseOK(response, sidecarTestContext);
             context.completeNow();
         });
     }
@@ -98,7 +97,7 @@
 
     void assertRingResponseOK(HttpResponse<Buffer> response, CassandraTestContext cassandraTestContext)
     {
-        IInstance instance = cassandraTestContext.cluster.getFirstRunningInstance();
+        IInstance instance = cassandraTestContext.getCluster().getFirstRunningInstance();
         IInstanceConfig config = instance.config();
         RingResponse ringResponse = response.bodyAsJson(RingResponse.class);
         assertThat(ringResponse).isNotNull()
diff --git a/src/test/integration/org/apache/cassandra/sidecar/routes/SnapshotsHandlerIntegrationTest.java b/src/test/integration/org/apache/cassandra/sidecar/routes/SnapshotsHandlerIntegrationTest.java
index 46ad70f..26b99f6 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/routes/SnapshotsHandlerIntegrationTest.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/routes/SnapshotsHandlerIntegrationTest.java
@@ -32,8 +32,8 @@
 import io.vertx.junit5.VertxTestContext;
 import org.apache.cassandra.sidecar.IntegrationTestBase;
 import org.apache.cassandra.sidecar.common.data.QualifiedTableName;
-import org.apache.cassandra.sidecar.common.testing.CassandraIntegrationTest;
-import org.apache.cassandra.sidecar.common.testing.CassandraTestContext;
+import org.apache.cassandra.sidecar.testing.CassandraSidecarTestContext;
+import org.apache.cassandra.testing.CassandraIntegrationTest;
 
 import static io.netty.handler.codec.http.HttpResponseStatus.OK;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -54,11 +54,10 @@
     }
 
     @CassandraIntegrationTest
-    void createSnapshotEndpointFailsWhenTableDoesNotExist(VertxTestContext context,
-                                                          CassandraTestContext cassandraTestContext)
+    void createSnapshotEndpointFailsWhenTableDoesNotExist(VertxTestContext context)
     throws InterruptedException
     {
-        createTestKeyspace(cassandraTestContext);
+        createTestKeyspace(sidecarTestContext);
 
         WebClient client = WebClient.create(vertx);
         String testRoute = "/api/v1/keyspaces/testkeyspace/tables/non-existent/snapshots/my-snapshot";
@@ -70,12 +69,11 @@
     }
 
     @CassandraIntegrationTest
-    void createSnapshotFailsWhenSnapshotAlreadyExists(VertxTestContext context,
-                                                      CassandraTestContext cassandraTestContext)
+    void createSnapshotFailsWhenSnapshotAlreadyExists(VertxTestContext context)
     throws InterruptedException
     {
-        createTestKeyspace(cassandraTestContext);
-        String table = createTestTableAndPopulate(cassandraTestContext);
+        createTestKeyspace(sidecarTestContext);
+        String table = createTestTableAndPopulate(sidecarTestContext);
 
         WebClient client = WebClient.create(vertx);
         String testRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
@@ -97,11 +95,11 @@
     }
 
     @CassandraIntegrationTest
-    void testCreateSnapshotEndpoint(VertxTestContext context, CassandraTestContext cassandraTestContext)
+    void testCreateSnapshotEndpoint(VertxTestContext context)
     throws InterruptedException
     {
-        createTestKeyspace(cassandraTestContext);
-        String table = createTestTableAndPopulate(cassandraTestContext);
+        createTestKeyspace(sidecarTestContext);
+        String table = createTestTableAndPopulate(sidecarTestContext);
 
         WebClient client = WebClient.create(vertx);
         String testRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/my-snapshot",
@@ -112,7 +110,7 @@
                   assertThat(response.statusCode()).isEqualTo(OK.code());
 
                   // validate that the snapshot is created
-                  final List<Path> found = findChildFile(cassandraTestContext, "127.0.0.1",
+                  final List<Path> found = findChildFile(sidecarTestContext, "127.0.0.1",
                                                          "my-snapshot");
                   assertThat(found).isNotEmpty();
 
@@ -135,24 +133,22 @@
     }
 
     @CassandraIntegrationTest
-    void deleteSnapshotFailsWhenTableDoesNotExist(VertxTestContext context,
-                                                  CassandraTestContext cassandraTestContext)
+    void deleteSnapshotFailsWhenTableDoesNotExist(VertxTestContext context)
     throws InterruptedException
     {
-        createTestKeyspace(cassandraTestContext);
-        createTestTableAndPopulate(cassandraTestContext);
+        createTestKeyspace(sidecarTestContext);
+        createTestTableAndPopulate(sidecarTestContext);
 
         String testRoute = "/api/v1/keyspaces/testkeyspace/tables/non-existent/snapshots/my-snapshot";
         assertNotFoundOnDeleteSnapshot(context, testRoute);
     }
 
     @CassandraIntegrationTest
-    void deleteSnapshotFailsWhenSnapshotDoesNotExist(VertxTestContext context,
-                                                     CassandraTestContext cassandraTestContext)
+    void deleteSnapshotFailsWhenSnapshotDoesNotExist(VertxTestContext context)
     throws InterruptedException
     {
-        createTestKeyspace(cassandraTestContext);
-        String table = createTestTableAndPopulate(cassandraTestContext);
+        createTestKeyspace(sidecarTestContext);
+        String table = createTestTableAndPopulate(sidecarTestContext);
 
         String testRoute = String.format("/api/v1/keyspaces/%s/tables/%s/snapshots/non-existent",
                                          TEST_KEYSPACE, table);
@@ -161,11 +157,11 @@
 
     @CassandraIntegrationTest(numDataDirsPerInstance = 1)
         // Set to > 1 to fail test
-    void testDeleteSnapshotEndpoint(VertxTestContext context, CassandraTestContext cassandraTestContext)
+    void testDeleteSnapshotEndpoint(VertxTestContext context)
     throws InterruptedException
     {
-        createTestKeyspace(cassandraTestContext);
-        String table = createTestTableAndPopulate(cassandraTestContext);
+        createTestKeyspace(sidecarTestContext);
+        String table = createTestTableAndPopulate(sidecarTestContext);
 
         WebClient client = WebClient.create(vertx);
         String snapshotName = "my-snapshot" + UUID.randomUUID();
@@ -180,7 +176,7 @@
               context.verify(() -> {
                   assertThat(createResponse.statusCode()).isEqualTo(OK.code());
                   final List<Path> found =
-                  findChildFile(cassandraTestContext, "127.0.0.1", snapshotName);
+                  findChildFile(sidecarTestContext, "127.0.0.1", snapshotName);
                   assertThat(found).isNotEmpty();
 
                   // snapshot directory exists inside data directory
@@ -195,7 +191,7 @@
                                        {
                                            assertThat(deleteResponse.statusCode()).isEqualTo(OK.code());
                                            final List<Path> found2 =
-                                           findChildFile(cassandraTestContext,
+                                           findChildFile(sidecarTestContext,
                                                          "127.0.0.1", snapshotName);
                                            assertThat(found2).isEmpty();
                                            context.completeNow();
@@ -205,7 +201,7 @@
         assertThat(context.awaitCompletion(30, TimeUnit.SECONDS)).isTrue();
     }
 
-    private String createTestTableAndPopulate(CassandraTestContext cassandraTestContext)
+    private String createTestTableAndPopulate(CassandraSidecarTestContext cassandraTestContext)
     {
         QualifiedTableName tableName = createTestTable(cassandraTestContext,
                                                        "CREATE TABLE %s (id text PRIMARY KEY, name text);");
diff --git a/src/test/integration/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableImportHandlerIntegrationTest.java b/src/test/integration/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableImportHandlerIntegrationTest.java
index 06b0b3e..56b6587 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableImportHandlerIntegrationTest.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/routes/sstableuploads/SSTableImportHandlerIntegrationTest.java
@@ -48,8 +48,8 @@
 import org.apache.cassandra.sidecar.IntegrationTestBase;
 import org.apache.cassandra.sidecar.common.SimpleCassandraVersion;
 import org.apache.cassandra.sidecar.common.data.QualifiedTableName;
-import org.apache.cassandra.sidecar.common.testing.CassandraIntegrationTest;
-import org.apache.cassandra.sidecar.common.testing.CassandraTestContext;
+import org.apache.cassandra.sidecar.testing.CassandraSidecarTestContext;
+import org.apache.cassandra.testing.CassandraIntegrationTest;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assumptions.assumeThat;
@@ -63,12 +63,12 @@
     public static final SimpleCassandraVersion MIN_VERSION_WITH_IMPORT = SimpleCassandraVersion.create("4.0.0");
 
     @CassandraIntegrationTest
-    void testSSTableImport(VertxTestContext context, CassandraTestContext cassandraTestContext)
+    void testSSTableImport(VertxTestContext vertxTestContext)
     throws IOException, InterruptedException
     {
         // Cassandra before 4.0 does not have the necessary JMX endpoints,
         // so we skip if the cluster version is below 4.0
-        assumeThat(cassandraTestContext.version)
+        assumeThat(sidecarTestContext.version)
         .withFailMessage("Import is only available in Cassandra 4.0 and later.")
         .isGreaterThanOrEqualTo(MIN_VERSION_WITH_IMPORT);
 
@@ -77,19 +77,19 @@
         // Test the import SSTable endpoint by importing data that was originally truncated.
         // Verify by querying the table contains all the results before truncation and after truncation.
 
-        Session session = maybeGetSession(cassandraTestContext);
-        createTestKeyspace(cassandraTestContext);
-        QualifiedTableName tableName = createTestTableAndPopulate(cassandraTestContext, Arrays.asList("a", "b"));
+        Session session = maybeGetSession(sidecarTestContext);
+        createTestKeyspace(sidecarTestContext);
+        QualifiedTableName tableName = createTestTableAndPopulate(sidecarTestContext, Arrays.asList("a", "b"));
 
         // create a snapshot called <tableName>-snapshot for tbl1
-        UpgradeableCluster cluster = cassandraTestContext.cluster;
+        UpgradeableCluster cluster = sidecarTestContext.cluster;
         final String snapshotStdout = cluster.get(1).nodetoolResult("snapshot",
                                                                     "--tag", tableName.tableName() + "-snapshot",
                                                                     "--table", tableName.tableName(),
                                                                     "--", tableName.keyspace()).getStdout();
         assertThat(snapshotStdout).contains("Snapshot directory: " + tableName.tableName() + "-snapshot");
         // find the directory in the filesystem
-        final List<Path> snapshotFiles = findChildFile(cassandraTestContext,
+        final List<Path> snapshotFiles = findChildFile(sidecarTestContext,
                                                        "127.0.0.1", tableName.tableName() + "-snapshot");
 
         assertThat(snapshotFiles).isNotEmpty();
@@ -101,8 +101,8 @@
         // verification happens in the host system. When calling import we use the same directory, but the
         // directory does not exist inside the cluster. For that reason we need to do the following to
         // ensure "import" finds the path inside the cluster
-        String uploadStagingDir = cassandraTestContext.getInstancesConfig()
-                                                      .instanceFromHost("127.0.0.1").stagingDir();
+        String uploadStagingDir = sidecarTestContext.getInstancesConfig()
+                                                    .instanceFromHost("127.0.0.1").stagingDir();
         final String stagingPathInContainer = uploadStagingDir + File.separator + uploadId
                                               + File.separator + tableName.keyspace()
                                               + File.separator + tableName.tableName();
@@ -121,7 +121,7 @@
         }
 
         // Now truncate the contents of the table
-        truncateAndVerify(cassandraTestContext, tableName);
+        truncateAndVerify(sidecarTestContext, tableName);
 
         // Add new data (c, d) to table
         populateTable(session, tableName, Arrays.asList("c", "d"));
@@ -129,28 +129,28 @@
         WebClient client = WebClient.create(vertx);
         String testRoute = "/api/v1/uploads/" + uploadId + "/keyspaces/" + tableName.keyspace()
                            + "/tables/" + tableName.tableName() + "/import";
-        sendRequest(context,
+        sendRequest(vertxTestContext,
                     () -> client.put(config.getPort(), "127.0.0.1", testRoute),
-                    context.succeeding(response -> context.verify(() -> {
+                    vertxTestContext.succeeding(response -> vertxTestContext.verify(() -> {
                         assertThat(response.statusCode()).isEqualTo(HttpResponseStatus.OK.code());
-                        assertThat(queryValues(cassandraTestContext, tableName))
+                        assertThat(queryValues(sidecarTestContext, tableName))
                         .containsAll(Arrays.asList("a", "b", "c", "d"));
-                        context.completeNow();
+                        vertxTestContext.completeNow();
                     })));
         // wait until test completes
-        assertThat(context.awaitCompletion(30, TimeUnit.SECONDS)).isTrue();
+        assertThat(vertxTestContext.awaitCompletion(30, TimeUnit.SECONDS)).isTrue();
     }
 
-    private void sendRequest(VertxTestContext context, Supplier<HttpRequest<Buffer>> requestSupplier,
+    private void sendRequest(VertxTestContext vertxTestContext, Supplier<HttpRequest<Buffer>> requestSupplier,
                              Handler<AsyncResult<HttpResponse<Buffer>>> handler)
     {
         requestSupplier.get()
-                       .send(context.succeeding(r -> context.verify(() -> {
+                       .send(vertxTestContext.succeeding(r -> vertxTestContext.verify(() -> {
                            int statusCode = r.statusCode();
                            if (statusCode == HttpResponseStatus.ACCEPTED.code())
                            {
                                // retry the request every second when the request is accepted
-                               vertx.setTimer(1000, tid -> sendRequest(context, requestSupplier, handler));
+                               vertx.setTimer(1000, tid -> sendRequest(vertxTestContext, requestSupplier, handler));
                            }
                            else
                            {
@@ -159,7 +159,8 @@
                        })));
     }
 
-    private void truncateAndVerify(CassandraTestContext cassandraTestContext, QualifiedTableName qualifiedTableName)
+    private void truncateAndVerify(CassandraSidecarTestContext cassandraTestContext,
+                                   QualifiedTableName qualifiedTableName)
     throws InterruptedException
     {
         Session session = maybeGetSession(cassandraTestContext);
@@ -174,7 +175,7 @@
         }
     }
 
-    private List<String> queryValues(CassandraTestContext cassandraTestContext, QualifiedTableName tableName)
+    private List<String> queryValues(CassandraSidecarTestContext cassandraTestContext, QualifiedTableName tableName)
     {
         Session session = maybeGetSession(cassandraTestContext);
         return session.execute("SELECT id FROM " + tableName)
@@ -184,7 +185,7 @@
                       .collect(Collectors.toList());
     }
 
-    private QualifiedTableName createTestTableAndPopulate(CassandraTestContext cassandraTestContext,
+    private QualifiedTableName createTestTableAndPopulate(CassandraSidecarTestContext cassandraTestContext,
                                                           List<String> values)
     {
         QualifiedTableName tableName = createTestTable(cassandraTestContext,
diff --git a/src/test/integration/org/apache/cassandra/sidecar/common/testing/CassandraTestContext.java b/src/test/integration/org/apache/cassandra/sidecar/testing/CassandraSidecarTestContext.java
similarity index 65%
rename from src/test/integration/org/apache/cassandra/sidecar/common/testing/CassandraTestContext.java
rename to src/test/integration/org/apache/cassandra/sidecar/testing/CassandraSidecarTestContext.java
index 157c422..52b4851 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/common/testing/CassandraTestContext.java
+++ b/src/test/integration/org/apache/cassandra/sidecar/testing/CassandraSidecarTestContext.java
@@ -16,10 +16,11 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.sidecar.common.testing;
+package org.apache.cassandra.sidecar.testing;
 
-import java.io.Closeable;
+import java.io.IOException;
 import java.net.InetSocketAddress;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
@@ -32,6 +33,7 @@
 import org.apache.cassandra.distributed.api.IInstanceConfig;
 import org.apache.cassandra.distributed.api.IUpgradeableInstance;
 import org.apache.cassandra.distributed.shared.JMXUtil;
+import org.apache.cassandra.sidecar.adapters.base.CassandraFactory;
 import org.apache.cassandra.sidecar.cluster.InstancesConfig;
 import org.apache.cassandra.sidecar.cluster.InstancesConfigImpl;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
@@ -40,34 +42,67 @@
 import org.apache.cassandra.sidecar.common.CassandraVersionProvider;
 import org.apache.cassandra.sidecar.common.JmxClient;
 import org.apache.cassandra.sidecar.common.SimpleCassandraVersion;
+import org.apache.cassandra.sidecar.common.dns.DnsResolver;
+import org.apache.cassandra.sidecar.common.utils.SidecarVersionProvider;
+import org.apache.cassandra.testing.CassandraTestContext;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
 /**
  * Passed to integration tests.
- * See {@link CassandraIntegrationTest} for the required annotation
- * See {@link CassandraTestTemplate} for the Test Template
  */
-public class CassandraTestContext implements Closeable
+public class CassandraSidecarTestContext extends CassandraTestContext
 {
     public final SimpleCassandraVersion version;
     public final UpgradeableCluster cluster;
     public final InstancesConfig instancesConfig;
     private List<CQLSessionProvider> sessionProviders;
     private List<JmxClient> jmxClients;
+    private static final SidecarVersionProvider svp = new SidecarVersionProvider("/sidecar.version");
 
-    CassandraTestContext(SimpleCassandraVersion version,
-                         UpgradeableCluster cluster,
-                         CassandraVersionProvider versionProvider)
+    private CassandraSidecarTestContext(SimpleCassandraVersion version,
+                                        UpgradeableCluster cluster,
+                                        CassandraVersionProvider versionProvider,
+                                        DnsResolver dnsResolver) throws IOException
     {
+        super(org.apache.cassandra.testing.SimpleCassandraVersion.create(version.major,
+                                                                         version.minor,
+                                                                         version.patch), cluster);
         this.version = version;
         this.cluster = cluster;
         this.sessionProviders = new ArrayList<>();
         this.jmxClients = new ArrayList<>();
-        this.instancesConfig = buildInstancesConfig(versionProvider);
+        this.instancesConfig = buildInstancesConfig(versionProvider, dnsResolver);
     }
 
-    private InstancesConfig buildInstancesConfig(CassandraVersionProvider versionProvider)
+    public static CassandraSidecarTestContext from(CassandraTestContext cassandraTestContext, DnsResolver dnsResolver)
+    {
+        org.apache.cassandra.testing.SimpleCassandraVersion rootVersion = cassandraTestContext.version;
+        SimpleCassandraVersion versionParsed = SimpleCassandraVersion.create(rootVersion.major,
+                                                                             rootVersion.minor,
+                                                                             rootVersion.patch);
+        CassandraVersionProvider versionProvider = cassandraVersionProvider(dnsResolver);
+        try
+        {
+            return new CassandraSidecarTestContext(versionParsed,
+                                                   cassandraTestContext.getCluster(),
+                                                   versionProvider,
+                                                   dnsResolver);
+        }
+        catch (IOException e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static CassandraVersionProvider cassandraVersionProvider(DnsResolver dnsResolver)
+    {
+        return new CassandraVersionProvider.Builder()
+               .add(new CassandraFactory(dnsResolver, svp.sidecarVersion())).build();
+    }
+
+    private InstancesConfig buildInstancesConfig(CassandraVersionProvider versionProvider,
+                                                 DnsResolver dnsResolver) throws IOException
     {
         List<InstanceMetadata> metadata = new ArrayList<>();
         for (int i = 0; i < cluster.size(); i++)
@@ -87,8 +122,10 @@
             // Use the parent of the first data directory as the staging directory
             Path dataDirParentPath = Paths.get(dataDirectories[0]).getParent();
             assertThat(dataDirParentPath).isNotNull()
-                                         .exists();
-            String uploadsStagingDirectory = dataDirParentPath.resolve("staging").toFile().getAbsolutePath();
+                      .exists();
+            Path stagingPath = dataDirParentPath.resolve("staging");
+            String uploadsStagingDirectory = stagingPath.toFile().getAbsolutePath();
+            Files.createDirectories(stagingPath);
             metadata.add(new InstanceMetadataImpl(i + 1,
                                                   config.broadcastAddress().getAddress().getHostAddress(),
                                                   nativeTransportPort,
@@ -99,7 +136,7 @@
                                                   versionProvider,
                                                   "1.0-TEST"));
         }
-        return new InstancesConfigImpl(metadata);
+        return new InstancesConfigImpl(metadata, dnsResolver);
     }
 
     private static int tryGetIntConfig(IInstanceConfig config, String configName, int defaultValue)
@@ -138,9 +175,11 @@
                ", cluster=" + cluster +
                '}';
     }
+
     @Override
     public void close()
     {
+        sessionProviders.forEach(CQLSessionProvider::close);
         instancesConfig.instances().forEach(instance -> instance.delegate().close());
     }
 
diff --git a/src/test/integration/org/apache/cassandra/testing/AbstractCassandraTestContext.java b/src/test/integration/org/apache/cassandra/testing/AbstractCassandraTestContext.java
new file mode 100644
index 0000000..4c4cb96
--- /dev/null
+++ b/src/test/integration/org/apache/cassandra/testing/AbstractCassandraTestContext.java
@@ -0,0 +1,49 @@
+/*
+ * 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 org.apache.cassandra.testing;
+
+import org.apache.cassandra.distributed.UpgradeableCluster;
+
+/**
+ * The base class for all CassandraTestContext implementations
+ */
+public abstract class AbstractCassandraTestContext implements AutoCloseable
+{
+    public final SimpleCassandraVersion version;
+    protected UpgradeableCluster cluster;
+
+    public AbstractCassandraTestContext(SimpleCassandraVersion version, UpgradeableCluster cluster)
+    {
+        this.version = version;
+        this.cluster = cluster;
+    }
+
+    public AbstractCassandraTestContext(SimpleCassandraVersion version)
+    {
+        this.version = version;
+    }
+
+    public void close() throws Exception
+    {
+        if (cluster != null)
+        {
+            cluster.close();
+        }
+    }
+}
diff --git a/src/test/integration/org/apache/cassandra/sidecar/common/testing/CassandraIntegrationTest.java b/src/test/integration/org/apache/cassandra/testing/CassandraIntegrationTest.java
similarity index 66%
rename from src/test/integration/org/apache/cassandra/sidecar/common/testing/CassandraIntegrationTest.java
rename to src/test/integration/org/apache/cassandra/testing/CassandraIntegrationTest.java
index 776bf24..efa7a12 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/common/testing/CassandraIntegrationTest.java
+++ b/src/test/integration/org/apache/cassandra/testing/CassandraIntegrationTest.java
@@ -16,12 +16,13 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.sidecar.common.testing;
+package org.apache.cassandra.testing;
 
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
+import java.util.function.Consumer;
 
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.TestTemplate;
@@ -41,11 +42,12 @@
      * Returns the number of initial nodes per datacenter for the integration tests. Defaults to 1 node per datacenter.
      *
      * @return the number of nodes per datacenter for the integration tests
-     * */
+     */
     int nodesPerDc() default 1;
 
     /**
      * Returns the number of nodes expected to be added by the end of the integration test. Defaults ot 0.
+     *
      * @return the number of nodes the test expects to add for the integration test.
      */
     int newNodesPerDc() default 0;
@@ -94,4 +96,32 @@
      */
     boolean nativeTransport() default true;
 
+    /**
+     * Return whether the cluster should be started before the test begins.
+     * It may be necessary to delay start/start in a thread if using ByteBuddy-based
+     * interception of cluster startup.
+     * @return true, if the cluster should be started before the test starts, false otherwise
+     */
+    boolean startCluster() default true;
+
+    /**
+     * Return whether the cluster should be built, or to simply add the cluster builder to the context.
+     * This may be useful in cases where the test requires more complex cluster startup.
+     * If false, the test should take an instance of {@link ConfigurableCassandraTestContext}
+     *     and call {@link ConfigurableCassandraTestContext#configureCluster(Consumer)}
+     *     or {@link ConfigurableCassandraTestContext#configureAndStartCluster(Consumer)} to get the cluster.
+     *     NOTE: This cluster object must be closed by the test as the framework doesn't have access to it.
+     * If true (the default), the test should take an instance of {@link CassandraTestContext}
+     *          {@link CassandraTestContext#getCluster()} will contain the built cluster.
+     * @return true if the cluster should be built by the test framework, false otherwise
+     */
+    boolean buildCluster() default true;
+
+    /**
+     * If the integration test does not need to be run on each version of Cassandra, set this to false
+     *      and it will be run only on the first version specified.
+     * @return true if the test should be run on all tested versions of Cassandra,
+     *         false if it should be run on the first version.
+     */
+    boolean versionDependent() default true;
 }
diff --git a/src/test/integration/org/apache/cassandra/testing/CassandraTestContext.java b/src/test/integration/org/apache/cassandra/testing/CassandraTestContext.java
new file mode 100644
index 0000000..9cceb6a
--- /dev/null
+++ b/src/test/integration/org/apache/cassandra/testing/CassandraTestContext.java
@@ -0,0 +1,50 @@
+/*
+ * 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 org.apache.cassandra.testing;
+
+import org.apache.cassandra.distributed.UpgradeableCluster;
+
+/**
+ * Passed to integration tests.
+ * See {@link CassandraIntegrationTest} for the required annotation
+ * See {@link CassandraTestTemplate} for the Test Template
+ */
+public class CassandraTestContext extends AbstractCassandraTestContext
+{
+
+    public CassandraTestContext(SimpleCassandraVersion version,
+                                UpgradeableCluster cluster)
+    {
+        super(version, cluster);
+    }
+
+    @Override
+    public String toString()
+    {
+        return "CassandraTestContext{"
+               + ", version=" + version
+               + ", cluster=" + cluster
+               + '}';
+    }
+
+    public UpgradeableCluster getCluster()
+    {
+        return cluster;
+    }
+}
diff --git a/src/test/integration/org/apache/cassandra/testing/CassandraTestTemplate.java b/src/test/integration/org/apache/cassandra/testing/CassandraTestTemplate.java
new file mode 100644
index 0000000..c613be4
--- /dev/null
+++ b/src/test/integration/org/apache/cassandra/testing/CassandraTestTemplate.java
@@ -0,0 +1,290 @@
+/*
+ * 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 org.apache.cassandra.testing;
+
+import java.lang.reflect.AnnotatedElement;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.Extension;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolver;
+import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
+import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.vdurmont.semver4j.Semver;
+import org.apache.cassandra.distributed.UpgradeableCluster;
+import org.apache.cassandra.distributed.api.Feature;
+import org.apache.cassandra.distributed.api.TokenSupplier;
+import org.apache.cassandra.distributed.shared.Versions;
+
+
+/**
+ * Creates a test per version of Cassandra we are testing
+ * Tests must be marked with {@link CassandraIntegrationTest}
+ * <p>
+ * This is a mix of parameterized tests + a custom extension.  we need to be able to provide the test context
+ * to each test (like an extension) but also need to create multiple tests (like parameterized tests).  Unfortunately
+ * the two don't play well with each other.  You can't get access to the parameters from the extension.
+ * This test template allows us full control of the test lifecycle and lets us tightly couple the context to each test
+ * we generate, since the same test can be run for multiple versions of C*.
+ */
+public class CassandraTestTemplate implements TestTemplateInvocationContextProvider
+{
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(CassandraTestTemplate.class);
+
+    private AbstractCassandraTestContext cassandraTestContext;
+
+    @Override
+    public boolean supportsTestTemplate(ExtensionContext context)
+    {
+        return true;
+    }
+
+    @Override
+    public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context)
+    {
+        CassandraIntegrationTest annotation = getCassandraIntegrationTestAnnotation(context, true);
+        if (annotation.versionDependent())
+        {
+            return new TestVersionSupplier().testVersions()
+                                            .map(v -> invocationContext(v, context));
+        }
+        return Stream.of(invocationContext(new TestVersionSupplier().testVersions().findFirst().get(), context));
+    }
+
+    /**
+     * Returns a {@link TestTemplateInvocationContext}
+     *
+     * @param version a version for the test
+     * @param context the <em>context</em> in which the current test or container is being executed.
+     * @return the <em>context</em> of a single invocation of a
+     * {@linkplain org.junit.jupiter.api.TestTemplate test template}
+     */
+    private TestTemplateInvocationContext invocationContext(TestVersion version, ExtensionContext context)
+    {
+        return new CassandraTestTemplateInvocationContext(context, version);
+    }
+
+    private static CassandraIntegrationTest getCassandraIntegrationTestAnnotation(ExtensionContext context,
+                                                                                  boolean throwIfNotFound)
+    {
+        Optional<AnnotatedElement> annotatedElement = context.getElement();
+        CassandraIntegrationTest result = annotatedElement.map(e -> e.getAnnotation(CassandraIntegrationTest.class))
+                                                          .orElse(null);
+        if (result == null && throwIfNotFound)
+        {
+            throw new RuntimeException("CassandraTestTemplate could not "
+                                       + "find @CassandraIntegrationTest annotation");
+        }
+        return result;
+    }
+
+    private final class CassandraTestTemplateInvocationContext implements TestTemplateInvocationContext
+    {
+        private final ExtensionContext context;
+        private final TestVersion version;
+
+        private CassandraTestTemplateInvocationContext(ExtensionContext context, TestVersion version)
+        {
+            this.context = context;
+            this.version = version;
+        }
+
+        /**
+         * A display name can be configured per test still - this adds the C* version we're testing automatically
+         * as a suffix to the name
+         *
+         * @param invocationIndex the index to the invocation
+         * @return the display name
+         */
+        @Override
+        public String getDisplayName(int invocationIndex)
+        {
+            return context.getDisplayName() + ": " + version.version();
+        }
+
+        /**
+         * Used to register the extensions required to start and stop the in-jvm dtest environment
+         *
+         * @return a list of registered {@link Extension extensions}
+         */
+        @Override
+        public List<Extension> getAdditionalExtensions()
+        {
+            return Arrays.asList(parameterResolver(), postProcessor(), beforeEach());
+        }
+
+        private BeforeEachCallback beforeEach()
+        {
+            return beforeEachCtx -> {
+                CassandraIntegrationTest annotation = getCassandraIntegrationTestAnnotation(context, true);
+                // spin up a C* cluster using the in-jvm dtest
+                Versions versions = Versions.find();
+                int nodesPerDc = annotation.nodesPerDc();
+                int dcCount = annotation.numDcs();
+                int newNodesPerDc = annotation.newNodesPerDc(); // if the test wants to add more nodes later
+                int finalNodeCount = dcCount * (nodesPerDc + newNodesPerDc);
+                Versions.Version requestedVersion = versions.getLatest(new Semver(version.version(),
+                                                                                  Semver.SemverType.LOOSE));
+                SimpleCassandraVersion versionParsed = SimpleCassandraVersion.create(version.version());
+
+                UpgradeableCluster.Builder clusterBuilder =
+                    UpgradeableCluster.build(nodesPerDc * dcCount)
+                                      .withVersion(requestedVersion)
+                                      .withDCs(dcCount)
+                                      .withDataDirCount(annotation.numDataDirsPerInstance())
+                                      .withConfig(config -> {
+                                      if (annotation.nativeTransport())
+                                      {
+                                          config.with(Feature.NATIVE_PROTOCOL);
+                                      }
+                                      if (annotation.jmx())
+                                      {
+                                          config.with(Feature.JMX);
+                                      }
+                                      if (annotation.gossip())
+                                      {
+                                          config.with(Feature.GOSSIP);
+                                      }
+                                      if (annotation.network())
+                                      {
+                                          config.with(Feature.NETWORK);
+                                      }
+                                  });
+                TokenSupplier tokenSupplier = TokenSupplier.evenlyDistributedTokens(finalNodeCount,
+                                                                                    clusterBuilder.getTokenCount());
+                clusterBuilder.withTokenSupplier(tokenSupplier);
+                if (annotation.buildCluster())
+                {
+                    UpgradeableCluster cluster;
+                    cluster = clusterBuilder.createWithoutStarting();
+                    if (annotation.startCluster())
+                    {
+                        cluster.startup();
+                    }
+                    cassandraTestContext = new CassandraTestContext(versionParsed, cluster);
+                }
+                else
+                {
+                    cassandraTestContext = new ConfigurableCassandraTestContext(versionParsed, clusterBuilder);
+                }
+                LOGGER.info("Testing {} against in-jvm dtest cluster", version);
+                LOGGER.info("Created Cassandra test context {}", cassandraTestContext);
+            };
+        }
+
+        /**
+         * Shuts down the in-jvm dtest cluster when the test is finished
+         *
+         * @return the {@link AfterTestExecutionCallback}
+         */
+        private AfterTestExecutionCallback postProcessor()
+        {
+            return postProcessorCtx -> {
+                if (cassandraTestContext != null)
+                {
+                    cassandraTestContext.close();
+                }
+            };
+        }
+
+        /**
+         * Required for Junit to know the CassandraTestContext can be used in these tests
+         *
+         * @return a {@link ParameterResolver}
+         */
+        private ParameterResolver parameterResolver()
+        {
+            return new ParameterResolver()
+            {
+                @Override
+                public boolean supportsParameter(ParameterContext parameterContext,
+                                                 ExtensionContext extensionContext)
+                {
+                    Class<?> parameterType = parameterContext.getParameter().getType();
+                    CassandraIntegrationTest annotation =
+                        getCassandraIntegrationTestAnnotation(extensionContext, false);
+                    if (annotation == null)
+                    {
+                        return false;
+                    }
+                    if (annotation.buildCluster())
+                    {
+                        if (parameterType.equals(CassandraTestContext.class))
+                        {
+                            return true;
+                        }
+                        else if (parameterType.equals(ConfigurableCassandraTestContext.class))
+                        {
+                            throw new IllegalArgumentException("CassandraIntegrationTest.buildCluster is true but"
+                                                               + " a configurable context was requested. Please "
+                                                               + "either request a CassandraTestContext "
+                                                               + "as a parameter or set buildCluster to false");
+                        }
+                    }
+                    else
+                    {
+                        if (parameterType.equals(ConfigurableCassandraTestContext.class))
+                        {
+                            return true;
+                        }
+                        else if (parameterType.equals(CassandraTestContext.class))
+                        {
+                            throw new IllegalArgumentException("CassandraIntegrationTest.buildCluster is false "
+                                                               + "but a built cluster was requested. Please "
+                                                               + "either request a "
+                                                               + "ConfigurableCassandraTestContext as a "
+                                                               + "parameter or set buildCluster to true"
+                                                               + "(the default)");
+                        }
+                    }
+                    return false;
+                }
+
+                @Override
+                public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
+                {
+                    return cassandraTestContext;
+                }
+            };
+        }
+    }
+
+    static
+    {
+        // Settings to reduce the test setup delay incurred if gossip is enabled
+        System.setProperty("cassandra.ring_delay_ms", "5000"); // down from 30s default
+        System.setProperty("cassandra.consistent.rangemovement", "false");
+        System.setProperty("cassandra.consistent.simultaneousmoves.allow", "true");
+        // End gossip delay settings
+        // Set the location of dtest jars
+        System.setProperty("cassandra.test.dtest_jar_path", "dtest-jars");
+        // Disable tcnative in netty as it can cause jni issues and logs lots errors
+        System.setProperty("cassandra.disable_tcactive_openssl", "true");
+    }
+}
diff --git a/src/test/integration/org/apache/cassandra/testing/ConfigurableCassandraTestContext.java b/src/test/integration/org/apache/cassandra/testing/ConfigurableCassandraTestContext.java
new file mode 100644
index 0000000..b8ed0cf
--- /dev/null
+++ b/src/test/integration/org/apache/cassandra/testing/ConfigurableCassandraTestContext.java
@@ -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 org.apache.cassandra.testing;
+
+import java.io.IOException;
+import java.util.function.Consumer;
+
+import org.apache.cassandra.distributed.UpgradeableCluster;
+
+/**
+ * A Cassandra Test Context implementation that allows advanced cluster configuration before cluster creation
+ * by providing access to the cluster builder.
+ */
+public class ConfigurableCassandraTestContext extends AbstractCassandraTestContext
+{
+    public static final String BUILT_CLUSTER_CANNOT_BE_CONFIGURED_ERROR =
+        "Cannot configure a cluster after it is built. Please set the buildCluster annotation attribute to false, "
+        + "and do not call `getCluster` before calling this method.";
+
+    private final UpgradeableCluster.Builder builder;
+
+    public ConfigurableCassandraTestContext(SimpleCassandraVersion version,
+                                            UpgradeableCluster.Builder builder)
+    {
+        super(version);
+        this.builder = builder;
+    }
+
+    public UpgradeableCluster configureCluster(Consumer<UpgradeableCluster.Builder> configurator) throws IOException
+    {
+        if (cluster != null)
+        {
+            throw new IllegalStateException(BUILT_CLUSTER_CANNOT_BE_CONFIGURED_ERROR);
+        }
+        configurator.accept(builder);
+        cluster = builder.createWithoutStarting();
+        return cluster;
+    }
+
+    public UpgradeableCluster configureAndStartCluster(Consumer<UpgradeableCluster.Builder> configurator)
+        throws IOException
+    {
+        cluster = configureCluster(configurator);
+        cluster.startup();
+        return cluster;
+    }
+
+    @Override
+    public String toString()
+    {
+        return "ConfigurableCassandraTestContext{"
+               + ", version=" + version
+               + ", builder=" + builder
+               + '}';
+    }
+}
diff --git a/src/test/integration/org/apache/cassandra/testing/SimpleCassandraVersion.java b/src/test/integration/org/apache/cassandra/testing/SimpleCassandraVersion.java
new file mode 100644
index 0000000..8b7be5e
--- /dev/null
+++ b/src/test/integration/org/apache/cassandra/testing/SimpleCassandraVersion.java
@@ -0,0 +1,172 @@
+/*
+ * 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 org.apache.cassandra.testing;
+
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Implements versioning used in Cassandra and CQL.
+ * <p>
+ * Note: The following code uses a slight variation from the semver document (http://semver.org).
+ * </p>
+ * <p>
+ * The rules here are a bit different than normal semver comparison.  For simplicity,
+ * an alpha version of 4.0 or a snapshot is equal to 4.0.  This allows us to test sidecar
+ * against alpha versions of a release.
+ * <p>
+ * While it's possible to implement full version comparison, it's likely not very useful
+ * This is because the main testing we are going to do will be against release versions - something like 4.0.
+ * We want to list an adapter as being compatible with 4.0 - and that should include 4.0 alpha, etc.
+ */
+public class SimpleCassandraVersion implements Comparable<SimpleCassandraVersion>
+{
+    /**
+     * note: 3rd group matches to words but only allows number and checked after regexp test.
+     * this is because 3rd and the last can be identical.
+     **/
+    private static final String VERSION_REGEXP = "(\\d+)\\.(\\d+)(?:\\.(\\w+))?(\\-[.\\w]+)?([.+][.\\w]+)?";
+
+    private static final Pattern PATTERN = Pattern.compile(VERSION_REGEXP);
+    private static final String SNAPSHOT = "-SNAPSHOT";
+
+    public final int major;
+    public final int minor;
+    public final int patch;
+
+    /**
+     * Parse a version from a string.
+     *
+     * @param version the string to parse
+     * @return the {@link SimpleCassandraVersion} parsed from the {@code version} string
+     * @throws IllegalArgumentException if the provided string does not
+     *                                  represent a version
+     */
+    public static SimpleCassandraVersion create(String version)
+    {
+        String stripped = version.toUpperCase().replace(SNAPSHOT, "");
+        Matcher matcher = PATTERN.matcher(stripped);
+        if (!matcher.matches())
+        {
+            throw new IllegalArgumentException("Invalid Cassandra version value: " + version);
+        }
+
+        try
+        {
+            int major = Integer.parseInt(matcher.group(1));
+            int minor = Integer.parseInt(matcher.group(2));
+            int patch = matcher.group(3) != null ? Integer.parseInt(matcher.group(3)) : 0;
+
+            return SimpleCassandraVersion.create(major, minor, patch);
+        }
+        catch (NumberFormatException e)
+        {
+            throw new IllegalArgumentException("Invalid Cassandra version value: " + version, e);
+        }
+    }
+
+    public static SimpleCassandraVersion create(int major, int minor, int patch)
+    {
+        if (major < 0 || minor < 0 || patch < 0)
+        {
+            throw new IllegalArgumentException();
+        }
+        return new SimpleCassandraVersion(major, minor, patch);
+    }
+
+
+    public SimpleCassandraVersion(int major, int minor, int patch)
+    {
+        this.major = major;
+        this.minor = minor;
+        this.patch = patch;
+    }
+
+
+    public int compareTo(SimpleCassandraVersion other)
+    {
+        if (major < other.major)
+        {
+            return -1;
+        }
+        if (major > other.major)
+        {
+            return 1;
+        }
+
+        if (minor < other.minor)
+        {
+            return -1;
+        }
+        if (minor > other.minor)
+        {
+            return 1;
+        }
+
+        if (patch < other.patch)
+        {
+            return -1;
+        }
+        if (patch > other.patch)
+        {
+            return 1;
+        }
+        return 0;
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if (!(o instanceof SimpleCassandraVersion))
+        {
+            return false;
+        }
+        SimpleCassandraVersion that = (SimpleCassandraVersion) o;
+        return major == that.major
+               && minor == that.minor
+               && patch == that.patch;
+    }
+
+    /**
+     * Returns true if this &gt; v2
+     *
+     * @param v2 the version to compare
+     * @return the result of the comparison
+     */
+    public boolean isGreaterThan(SimpleCassandraVersion v2)
+    {
+        return compareTo(v2) > 0;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Objects.hash(major, minor, patch);
+    }
+
+    @Override
+    public String toString()
+    {
+        StringBuilder sb = new StringBuilder();
+        sb.append(major).append('.').append(minor).append('.').append(patch);
+
+        return sb.toString();
+    }
+}
diff --git a/src/test/integration/org/apache/cassandra/sidecar/common/testing/TestVersion.java b/src/test/integration/org/apache/cassandra/testing/TestVersion.java
similarity index 95%
rename from src/test/integration/org/apache/cassandra/sidecar/common/testing/TestVersion.java
rename to src/test/integration/org/apache/cassandra/testing/TestVersion.java
index 956bc8a..7fb21f0 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/common/testing/TestVersion.java
+++ b/src/test/integration/org/apache/cassandra/testing/TestVersion.java
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.sidecar.common.testing;
+package org.apache.cassandra.testing;
 
 /**
  * Works with {@link TestVersionSupplier}
diff --git a/src/test/integration/org/apache/cassandra/sidecar/common/testing/TestVersionSupplier.java b/src/test/integration/org/apache/cassandra/testing/TestVersionSupplier.java
similarity index 96%
rename from src/test/integration/org/apache/cassandra/sidecar/common/testing/TestVersionSupplier.java
rename to src/test/integration/org/apache/cassandra/testing/TestVersionSupplier.java
index 2822a0a..46c7958 100644
--- a/src/test/integration/org/apache/cassandra/sidecar/common/testing/TestVersionSupplier.java
+++ b/src/test/integration/org/apache/cassandra/testing/TestVersionSupplier.java
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.sidecar.common.testing;
+package org.apache.cassandra.testing;
 
 import java.util.Arrays;
 import java.util.stream.Stream;
diff --git a/src/test/java/org/apache/cassandra/sidecar/MainModuleTest.java b/src/test/java/org/apache/cassandra/sidecar/MainModuleTest.java
index 8440af0..cc0efd0 100644
--- a/src/test/java/org/apache/cassandra/sidecar/MainModuleTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/MainModuleTest.java
@@ -23,7 +23,7 @@
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 import com.google.inject.util.Modules;
-import org.apache.cassandra.sidecar.utils.SidecarVersionProvider;
+import org.apache.cassandra.sidecar.common.utils.SidecarVersionProvider;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
diff --git a/src/test/java/org/apache/cassandra/sidecar/TestModule.java b/src/test/java/org/apache/cassandra/sidecar/TestModule.java
index 4723944..9b6b2f9 100644
--- a/src/test/java/org/apache/cassandra/sidecar/TestModule.java
+++ b/src/test/java/org/apache/cassandra/sidecar/TestModule.java
@@ -35,6 +35,7 @@
 import org.apache.cassandra.sidecar.common.MockCassandraFactory;
 import org.apache.cassandra.sidecar.common.NodeSettings;
 import org.apache.cassandra.sidecar.common.TestValidationConfiguration;
+import org.apache.cassandra.sidecar.common.dns.DnsResolver;
 import org.apache.cassandra.sidecar.common.utils.ValidationConfiguration;
 import org.apache.cassandra.sidecar.config.CacheConfiguration;
 import org.apache.cassandra.sidecar.config.WorkerPoolConfiguration;
@@ -86,9 +87,9 @@
 
     @Provides
     @Singleton
-    public InstancesConfig instancesConfig()
+    public InstancesConfig instancesConfig(DnsResolver dnsResolver)
     {
-        return new InstancesConfigImpl(instancesMetas());
+        return new InstancesConfigImpl(instancesMetas(), dnsResolver);
     }
 
     public List<InstanceMetadata> instancesMetas()
diff --git a/src/test/java/org/apache/cassandra/sidecar/YAMLSidecarConfigurationTest.java b/src/test/java/org/apache/cassandra/sidecar/YAMLSidecarConfigurationTest.java
index 82dbc68..90f4033 100644
--- a/src/test/java/org/apache/cassandra/sidecar/YAMLSidecarConfigurationTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/YAMLSidecarConfigurationTest.java
@@ -27,6 +27,7 @@
 import org.apache.cassandra.sidecar.cluster.InstancesConfig;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
 import org.apache.cassandra.sidecar.common.CassandraVersionProvider;
+import org.apache.cassandra.sidecar.common.dns.DnsResolver;
 import org.apache.cassandra.sidecar.common.utils.ValidationConfiguration;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -44,20 +45,29 @@
     @Test
     public void testSidecarConfiguration() throws IOException
     {
-        Configuration multipleInstancesConfig = YAMLSidecarConfiguration.of(
-                confPath("sidecar_multiple_instances.yaml"), versionProvider, SIDECAR_VERSION);
+        String confPath1 = confPath("sidecar_multiple_instances.yaml");
+        Configuration multipleInstancesConfig = YAMLSidecarConfiguration.of(confPath1,
+                                                                            versionProvider,
+                                                                            SIDECAR_VERSION,
+                                                                            DnsResolver.DEFAULT);
         validateSidecarConfiguration(multipleInstancesConfig);
 
-        Configuration singleInstanceConfig = YAMLSidecarConfiguration.of(
-                confPath("sidecar_single_instance.yaml"), versionProvider, SIDECAR_VERSION);
+        String confPath = confPath("sidecar_single_instance.yaml");
+        Configuration singleInstanceConfig = YAMLSidecarConfiguration.of(confPath,
+                                                                  versionProvider,
+                                                                  SIDECAR_VERSION,
+                                                                  DnsResolver.DEFAULT);
         validateSidecarConfiguration(singleInstanceConfig);
     }
 
     @Test
     public void testLegacySidecarYAMLFormatWithSingleInstance() throws IOException
     {
-        Configuration configuration = YAMLSidecarConfiguration.of(
-                confPath("sidecar_single_instance.yaml"), versionProvider, SIDECAR_VERSION);
+        String confPath = confPath("sidecar_single_instance.yaml");
+        Configuration configuration = YAMLSidecarConfiguration.of(confPath,
+                                                                  versionProvider,
+                                                                  SIDECAR_VERSION,
+                                                                  DnsResolver.DEFAULT);
         InstancesConfig instancesConfig = configuration.getInstancesConfig();
         assertThat(instancesConfig.instances().size()).isEqualTo(1);
         InstanceMetadata instanceMetadata = instancesConfig.instances().get(0);
@@ -68,20 +78,29 @@
     @Test
     public void testReadAllowableTimeSkew() throws IOException
     {
-        Configuration configuration = YAMLSidecarConfiguration.of(
-                confPath("sidecar_single_instance.yaml"), versionProvider, SIDECAR_VERSION);
+        String confPath1 = confPath("sidecar_single_instance.yaml");
+        Configuration configuration = YAMLSidecarConfiguration.of(confPath1,
+                                                                  versionProvider,
+                                                                  SIDECAR_VERSION,
+                                                                  DnsResolver.DEFAULT);
         assertThat(configuration.allowableSkewInMinutes()).isEqualTo(89);
 
-        configuration = YAMLSidecarConfiguration.of(
-                confPath("sidecar_custom_allowable_time_skew.yaml"), versionProvider, SIDECAR_VERSION);
+        String confPath = confPath("sidecar_custom_allowable_time_skew.yaml");
+        configuration = YAMLSidecarConfiguration.of(confPath,
+                                                    versionProvider,
+                                                    SIDECAR_VERSION,
+                                                    DnsResolver.DEFAULT);
         assertThat(configuration.allowableSkewInMinutes()).isEqualTo(1);
     }
 
     @Test
     public void testReadingSingleInstanceSectionOverMultipleInstances() throws IOException
     {
-        Configuration configuration = YAMLSidecarConfiguration.of(
-                confPath("sidecar_with_single_multiple_instances.yaml"), versionProvider, SIDECAR_VERSION);
+        String confPath = confPath("sidecar_with_single_multiple_instances.yaml");
+        Configuration configuration = YAMLSidecarConfiguration.of(confPath,
+                                                                  versionProvider,
+                                                                  SIDECAR_VERSION,
+                                                                  DnsResolver.DEFAULT);
         InstancesConfig instancesConfig = configuration.getInstancesConfig();
         assertThat(instancesConfig.instances().size()).isEqualTo(1);
         InstanceMetadata instanceMetadata = instancesConfig.instances().get(0);
@@ -92,8 +111,11 @@
     @Test
     public void testReadingMultipleInstances() throws IOException
     {
-        Configuration configuration =  YAMLSidecarConfiguration.of(
-                confPath("sidecar_multiple_instances.yaml"), versionProvider, SIDECAR_VERSION);
+        String confPath = confPath("sidecar_multiple_instances.yaml");
+        Configuration configuration = YAMLSidecarConfiguration.of(confPath,
+                                                                  versionProvider,
+                                                                  SIDECAR_VERSION,
+                                                                  DnsResolver.DEFAULT);
         InstancesConfig instancesConfig = configuration.getInstancesConfig();
         assertThat(instancesConfig.instances().size()).isEqualTo(2);
     }
@@ -101,8 +123,11 @@
     @Test
     public void testReadingCassandraInputValidation() throws IOException
     {
-        Configuration configuration = YAMLSidecarConfiguration.of(
-                confPath("sidecar_validation_configuration.yaml"), versionProvider, SIDECAR_VERSION);
+        String confPath = confPath("sidecar_validation_configuration.yaml");
+        Configuration configuration = YAMLSidecarConfiguration.of(confPath,
+                                                                  versionProvider,
+                                                                  SIDECAR_VERSION,
+                                                                  DnsResolver.DEFAULT);
         ValidationConfiguration validationConfiguration = configuration.getValidationConfiguration();
 
         assertThat(validationConfiguration.forbiddenKeyspaces()).contains("a", "b", "c");
@@ -116,8 +141,11 @@
     @Test
     public void testUploadsConfiguration() throws IOException
     {
-        Configuration configuration = YAMLSidecarConfiguration.of(
-                confPath("sidecar_multiple_instances.yaml"), versionProvider, SIDECAR_VERSION);
+        String confPath = confPath("sidecar_multiple_instances.yaml");
+        Configuration configuration = YAMLSidecarConfiguration.of(confPath,
+                                                                  versionProvider,
+                                                                  SIDECAR_VERSION,
+                                                                  DnsResolver.DEFAULT);
 
         assertThat(configuration.getConcurrentUploadsLimit()).isEqualTo(80);
         assertThat(configuration.getMinSpacePercentRequiredForUpload()).isEqualTo(10);
diff --git a/src/test/java/org/apache/cassandra/sidecar/routes/SchemaHandlerTest.java b/src/test/java/org/apache/cassandra/sidecar/routes/SchemaHandlerTest.java
index 3cead95..3b98fdb 100644
--- a/src/test/java/org/apache/cassandra/sidecar/routes/SchemaHandlerTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/routes/SchemaHandlerTest.java
@@ -53,7 +53,7 @@
 import org.apache.cassandra.sidecar.cluster.InstancesConfig;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
 import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
-import org.apache.cassandra.sidecar.utils.IOUtils;
+import org.apache.cassandra.sidecar.common.utils.IOUtils;
 
 import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
 import static io.netty.handler.codec.http.HttpResponseStatus.OK;
diff --git a/src/test/java/org/apache/cassandra/sidecar/snapshots/SnapshotUtils.java b/src/test/java/org/apache/cassandra/sidecar/snapshots/SnapshotUtils.java
index 0e1d6a6..7dc89de 100644
--- a/src/test/java/org/apache/cassandra/sidecar/snapshots/SnapshotUtils.java
+++ b/src/test/java/org/apache/cassandra/sidecar/snapshots/SnapshotUtils.java
@@ -36,6 +36,7 @@
 import org.apache.cassandra.sidecar.common.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.common.CassandraVersionProvider;
 import org.apache.cassandra.sidecar.common.MockCassandraFactory;
+import org.apache.cassandra.sidecar.common.dns.DnsResolver;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
@@ -129,7 +130,7 @@
                                                                    makeStagingDir(rootPath),
                                                                    delegate2);
         List<InstanceMetadata> instanceMetas = Arrays.asList(localhost, localhost2);
-        return new InstancesConfigImpl(instanceMetas);
+        return new InstancesConfigImpl(instanceMetas, DnsResolver.DEFAULT);
     }
 
     public static List<String[]> mockSnapshotDirectories()