diff --git a/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseHttpContainer.java b/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseHttpContainer.java new file mode 100644 index 00000000000..175a5aa8598 --- /dev/null +++ b/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseHttpContainer.java @@ -0,0 +1,150 @@ +package org.testcontainers.clickhouse; + +import lombok.Getter; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +import java.time.Duration; + +/** + * Testcontainers implementation for ClickHouse with HTTP API support. + *

+ * This container provides access to ClickHouse's HTTP interface for executing queries + * and managing the database via REST API calls. + *

+ * Supported image: {@code clickhouse/clickhouse-server} + *

+ * Exposed ports: + *

+ */ +public class ClickHouseHttpContainer extends GenericContainer { + + static final String CLICKHOUSE_CLICKHOUSE_SERVER = "clickhouse/clickhouse-server"; + + private static final DockerImageName CLICKHOUSE_IMAGE_NAME = DockerImageName.parse(CLICKHOUSE_CLICKHOUSE_SERVER); + + static final Integer HTTP_PORT = 8123; + + static final Integer NATIVE_PORT = 9000; + + static final String DEFAULT_USER = "test"; + + static final String DEFAULT_PASSWORD = "test"; + + @Getter + private String databaseName = "default"; + + @Getter + private String username = DEFAULT_USER; + + @Getter + private String password = DEFAULT_PASSWORD; + + public ClickHouseHttpContainer(String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + public ClickHouseHttpContainer(final DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(CLICKHOUSE_IMAGE_NAME); + + addExposedPorts(HTTP_PORT, NATIVE_PORT); + waitingFor( + Wait + .forHttp("/") + .forPort(HTTP_PORT) + .forStatusCode(200) + .forResponsePredicate("Ok."::equals) + .withStartupTimeout(Duration.ofMinutes(1)) + ); + } + + @Override + protected void configure() { + withEnv("CLICKHOUSE_DB", this.databaseName); + withEnv("CLICKHOUSE_USER", this.username); + withEnv("CLICKHOUSE_PASSWORD", this.password); + } + + /** + * Gets the HTTP URL for the ClickHouse HTTP API. + * + * @return the HTTP URL + */ + public String getHttpUrl() { + return String.format("http://%s:%d", getHost(), getMappedPort(HTTP_PORT)); + } + + /** + * Gets the HTTP URL with database path. + * + * @return the HTTP URL with database path + */ + public String getHttpUrl(String database) { + return String.format("http://%s:%d/?database=%s", getHost(), getMappedPort(HTTP_PORT), database); + } + + /** + * Gets the HTTP host and port address. + * + * @return the HTTP host and port + */ + public String getHttpHostAddress() { + return getHost() + ":" + getMappedPort(HTTP_PORT); + } + + /** + * Gets the mapped HTTP port. + * + * @return the mapped HTTP port + */ + public Integer getHttpPort() { + return getMappedPort(HTTP_PORT); + } + + /** + * Gets the mapped native port. + * + * @return the mapped native port + */ + public Integer getNativePort() { + return getMappedPort(NATIVE_PORT); + } + + /** + * Sets the database name. + * + * @param databaseName the database name + * @return this container instance + */ + public ClickHouseHttpContainer withDatabaseName(String databaseName) { + this.databaseName = databaseName; + return this; + } + + /** + * Sets the username. + * + * @param username the username + * @return this container instance + */ + public ClickHouseHttpContainer withUsername(String username) { + this.username = username; + return this; + } + + /** + * Sets the password. + * + * @param password the password + * @return this container instance + */ + public ClickHouseHttpContainer withPassword(String password) { + this.password = password; + return this; + } +} diff --git a/modules/clickhouse/src/test/java/org/testcontainers/clickhouse/ClickHouseHttpContainerTest.java b/modules/clickhouse/src/test/java/org/testcontainers/clickhouse/ClickHouseHttpContainerTest.java new file mode 100644 index 00000000000..1933e1b58d5 --- /dev/null +++ b/modules/clickhouse/src/test/java/org/testcontainers/clickhouse/ClickHouseHttpContainerTest.java @@ -0,0 +1,148 @@ +package org.testcontainers.clickhouse; + +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.ParseException; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.junit.Test; +import org.testcontainers.ClickhouseTestImages; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ClickHouseHttpContainerTest { + + @Test + public void testSimpleHttpQuery() throws IOException, ParseException { + try (ClickHouseHttpContainer clickhouse = new ClickHouseHttpContainer(ClickhouseTestImages.CLICKHOUSE_IMAGE)) { + clickhouse.start(); + + String result = executeHttpQuery(clickhouse, "SELECT 1"); + assertThat(result.trim()).isEqualTo("1"); + } + } + + @Test + public void testCustomCredentials() throws IOException, ParseException { + try ( + ClickHouseHttpContainer clickhouse = new ClickHouseHttpContainer(ClickhouseTestImages.CLICKHOUSE_IMAGE) + .withUsername("custom_user") + .withPassword("custom_password") + .withDatabaseName("custom_db") + ) { + assertThat(clickhouse.getUsername()).isEqualTo("custom_user"); + assertThat(clickhouse.getPassword()).isEqualTo("custom_password"); + assertThat(clickhouse.getDatabaseName()).isEqualTo("custom_db"); + + clickhouse.start(); + + String result = executeHttpQuery(clickhouse, "SELECT 2"); + assertThat(result.trim()).isEqualTo("2"); + } + } + + @Test + public void testHttpUrlMethods() { + try (ClickHouseHttpContainer clickhouse = new ClickHouseHttpContainer(ClickhouseTestImages.CLICKHOUSE_IMAGE)) { + clickhouse.start(); + + String httpUrl = clickhouse.getHttpUrl(); + assertThat(httpUrl).matches("http://localhost:\\d+"); + + String httpUrlWithDb = clickhouse.getHttpUrl("test_db"); + assertThat(httpUrlWithDb).matches("http://localhost:\\d+/\\?database=test_db"); + + String hostAddress = clickhouse.getHttpHostAddress(); + assertThat(hostAddress).matches("localhost:\\d+"); + + Integer httpPort = clickhouse.getHttpPort(); + assertThat(httpPort).isGreaterThan(0); + + Integer nativePort = clickhouse.getNativePort(); + assertThat(nativePort).isGreaterThan(0); + assertThat(nativePort).isNotEqualTo(httpPort); + } + } + + @Test + public void testCreateTableAndInsert() throws IOException, ParseException { + try (ClickHouseHttpContainer clickhouse = new ClickHouseHttpContainer(ClickhouseTestImages.CLICKHOUSE_IMAGE)) { + clickhouse.start(); + + // Create table + executeHttpQuery(clickhouse, "CREATE TABLE test_table (id UInt32, name String) ENGINE = Memory"); + + // Insert data + executeHttpQuery(clickhouse, "INSERT INTO test_table VALUES (1, 'test')"); + + // Query data + String result = executeHttpQuery(clickhouse, "SELECT id, name FROM test_table"); + assertThat(result.trim()).isEqualTo("1\ttest"); + } + } + + @Test + public void testHealthCheck() throws IOException, ParseException { + try (ClickHouseHttpContainer clickhouse = new ClickHouseHttpContainer(ClickhouseTestImages.CLICKHOUSE_IMAGE)) { + clickhouse.start(); + + try (CloseableHttpClient client = HttpClients.createDefault()) { + HttpGet request = new HttpGet(clickhouse.getHttpUrl()); + + try (CloseableHttpResponse response = client.execute(request)) { + assertThat(response.getCode()).isEqualTo(200); + String body = EntityUtils.toString(response.getEntity()); + assertThat(body.trim()).isEqualTo("Ok."); + } + } + } + } + + @Test + public void testNewVersionAuth() throws IOException, ParseException { + try ( + ClickHouseHttpContainer clickhouse = new ClickHouseHttpContainer( + ClickhouseTestImages.CLICKHOUSE_24_12_IMAGE + ) + ) { + clickhouse.start(); + + String result = executeHttpQuery(clickhouse, "SELECT 1"); + assertThat(result.trim()).isEqualTo("1"); + } + } + + private String executeHttpQuery(ClickHouseHttpContainer container, String query) + throws IOException, ParseException { + try (CloseableHttpClient client = HttpClients.createDefault()) { + String url = container.getHttpUrl() + "/?database=" + container.getDatabaseName(); + HttpPost request = new HttpPost(url); + + String auth = container.getUsername() + ":" + container.getPassword(); + String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); + request.setHeader("Authorization", "Basic " + encodedAuth); + + StringEntity entity = new StringEntity(query, ContentType.TEXT_PLAIN); + request.setEntity(entity); + + try (CloseableHttpResponse response = client.execute(request)) { + if (response.getCode() != 200) { + String errorBody = EntityUtils.toString(response.getEntity()); + throw new RuntimeException( + "HTTP request failed with status " + response.getCode() + ": " + errorBody + ); + } + + return EntityUtils.toString(response.getEntity()); + } + } + } +}