From a4b6e202a179d3d814652523171a099a029a0fe9 Mon Sep 17 00:00:00 2001 From: Srinivasa Vasu Date: Sun, 15 Aug 2021 15:13:46 +0530 Subject: [PATCH] yugabytedb - initial commit Signed-off-by: Srinivasa Vasu --- docs/modules/databases/yugabytedb.md | 46 +++++ modules/yugabytedb/build.gradle | 10 ++ .../YugabyteContainerConstants.java | 42 +++++ .../containers/YugabyteYCQLContainer.java | 160 ++++++++++++++++++ .../containers/YugabyteYSQLContainer.java | 137 +++++++++++++++ .../YugabyteYSQLContainerProvider.java | 38 +++++ .../delegate/YugabyteYCQLDelegate.java | 41 +++++ .../strategy/YugabyteYCQLWaitStrategy.java | 50 ++++++ .../strategy/YugabyteYSQLWaitStrategy.java | 56 ++++++ ...s.containers.JdbcDatabaseContainerProvider | 1 + .../YugabyteTestContainerConstants.java | 18 ++ .../YugabyteYSQLJDBCDriverTest.java | 25 +++ .../yugabytedb/YugabyteYCQLUnitTest.java | 88 ++++++++++ .../yugabytedb/YugabyteYSQLUnitTest.java | 72 ++++++++ .../src/test/resources/init/init_yql.sql | 5 + 15 files changed, 789 insertions(+) create mode 100644 docs/modules/databases/yugabytedb.md create mode 100644 modules/yugabytedb/build.gradle create mode 100644 modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteContainerConstants.java create mode 100644 modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteYCQLContainer.java create mode 100644 modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteYSQLContainer.java create mode 100644 modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteYSQLContainerProvider.java create mode 100644 modules/yugabytedb/src/main/java/org/testcontainers/containers/delegate/YugabyteYCQLDelegate.java create mode 100644 modules/yugabytedb/src/main/java/org/testcontainers/containers/strategy/YugabyteYCQLWaitStrategy.java create mode 100644 modules/yugabytedb/src/main/java/org/testcontainers/containers/strategy/YugabyteYSQLWaitStrategy.java create mode 100644 modules/yugabytedb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider create mode 100644 modules/yugabytedb/src/test/java/org/testcontainers/YugabyteTestContainerConstants.java create mode 100644 modules/yugabytedb/src/test/java/org/testcontainers/jdbc/yugabytedb/YugabyteYSQLJDBCDriverTest.java create mode 100644 modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteYCQLUnitTest.java create mode 100644 modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteYSQLUnitTest.java create mode 100644 modules/yugabytedb/src/test/resources/init/init_yql.sql diff --git a/docs/modules/databases/yugabytedb.md b/docs/modules/databases/yugabytedb.md new file mode 100644 index 00000000000..47204a8cdb9 --- /dev/null +++ b/docs/modules/databases/yugabytedb.md @@ -0,0 +1,46 @@ +# YugabyteDB Module + +See [Database containers](./index.md) for documentation and usage that is common to all database container types. + +YugabyteDB supports two APIs. +- Yugabyte Structured Query Language [YSQL](https://docs.yugabyte.com/latest/api/ysql/) is a fully-relational API that is built by the PostgreSQL code +- Yugabyte Cloud Query Language [YCQL](https://docs.yugabyte.com/latest/api/ycql/) is a semi-relational SQL API that has its roots in the Cassandra Query Language + +## Usage example + +### YSQL API + +```java +public class YugabyteDBTest { + + @Rule + public YugabyteYSQLContainer container = new YugabyteYSQLContainer("yugabytedb/yugabyte:2.7.2.0-b216"); + + @Test + public void method() { + String url = container.getJdbcUrl(); + + ... create a connection and run the tests as usual. It also depends on the frameworks being used. +``` + +#### JDBC URL + +`jdbc:tc:yugabyte:2.7.2.0-b216:///yugabyte` + +### YCQL API + +```java +public class YugabyteDBTest { + + @Rule + public YugabyteYCQLContainer container = new YugabyteYCQLContainer("yugabytedb/yugabyte:2.7.2.0-b216"); + + @Test + public void method() { + Session session = container.getSession(); + + ... create a connection and run the tests as usual. It also depends on the frameworks being used. +``` + +## Adding this module to your project dependencies +[[TODO]] diff --git a/modules/yugabytedb/build.gradle b/modules/yugabytedb/build.gradle new file mode 100644 index 00000000000..7992e09b95a --- /dev/null +++ b/modules/yugabytedb/build.gradle @@ -0,0 +1,10 @@ +description = "Testcontainers :: JDBC :: YugabyteDB" + +dependencies { + api project(':jdbc') + testImplementation project(':jdbc-test') + // YCQL driver + implementation 'com.yugabyte:java-driver-core:4.6.0-yb-6' + // YSQL driver + testImplementation 'com.yugabyte:jdbc-yugabytedb:42.2.7-yb-5-beta.2' +} diff --git a/modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteContainerConstants.java b/modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteContainerConstants.java new file mode 100644 index 00000000000..53a716fa963 --- /dev/null +++ b/modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteContainerConstants.java @@ -0,0 +1,42 @@ +package org.testcontainers.containers; + +import org.testcontainers.utility.DockerImageName; + +/** + * Constants used in both YCQL and YSQL APIs + * + * @author srinivasa-vasu + */ +public interface YugabyteContainerConstants { + + DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("yugabytedb/yugabyte"); + + String DEFAULT_TAG = "2.7.2.0-b216"; + + String NAME = "yugabyte"; + + Integer YSQL_PORT = 5433; + + Integer YCQL_PORT = 9042; + + Integer MASTER_DASHBOARD_PORT = 7000; + + Integer TSERVER_DASHBOARD_PORT = 9000; + + String JDBC_DRIVER_CLASS = "org.postgresql.Driver"; + + String JDBC_CONNECT_PREFIX = "jdbc:postgresql"; + + String ENTRYPOINT = "bin/yugabyted start --daemon=false"; + + String LOCAL_DC = "datacenter1"; + + String USER_PARAM = "user"; + + String PASSWORD_PARAM = "password"; + + String YSQL_TEST_QUERY = "SELECT 1"; + + String YCQL_TEST_QUERY = "SELECT release_version FROM system.local"; + +} diff --git a/modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteYCQLContainer.java b/modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteYCQLContainer.java new file mode 100644 index 00000000000..a46f8fec4b8 --- /dev/null +++ b/modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteYCQLContainer.java @@ -0,0 +1,160 @@ +package org.testcontainers.containers; + +import java.net.InetSocketAddress; +import java.time.Duration; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.CqlSessionBuilder; +import com.github.dockerjava.api.command.InspectContainerResponse; +import org.testcontainers.containers.delegate.YugabyteYCQLDelegate; +import org.testcontainers.containers.strategy.YugabyteYCQLWaitStrategy; +import org.testcontainers.ext.ScriptUtils; +import org.testcontainers.utility.DockerImageName; + +import static org.testcontainers.containers.YugabyteContainerConstants.DEFAULT_IMAGE_NAME; +import static org.testcontainers.containers.YugabyteContainerConstants.ENTRYPOINT; +import static org.testcontainers.containers.YugabyteContainerConstants.LOCAL_DC; +import static org.testcontainers.containers.YugabyteContainerConstants.MASTER_DASHBOARD_PORT; +import static org.testcontainers.containers.YugabyteContainerConstants.TSERVER_DASHBOARD_PORT; +import static org.testcontainers.containers.YugabyteContainerConstants.YCQL_PORT; + +/** + * YugabyteDB YCQL (Cloud Query Language) API container + * + * @author srinivasa-vasu + * @see YCQL API + */ +public class YugabyteYCQLContainer extends GenericContainer { + + private String keyspace; + + private String username; + + private String password; + + private String initScript; + + /** + * @param imageName image name + */ + public YugabyteYCQLContainer(final String imageName) { + this(DockerImageName.parse(imageName)); + } + + /** + * @param imageName image name + */ + public YugabyteYCQLContainer(final DockerImageName imageName) { + super(imageName); + imageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + withExposedPorts(YCQL_PORT, MASTER_DASHBOARD_PORT, TSERVER_DASHBOARD_PORT); + waitingFor(new YugabyteYCQLWaitStrategy(this).withStartupTimeout(Duration.ofSeconds(60))); + withCommand(ENTRYPOINT); + } + + /** + * Configures the environment variables. Setting up these variables would create the + * custom objects. Setting {@link #withKeyspaceName(String)}, + * {@link #withUsername(String)}, {@link #withPassword(String)} these parameters will + * initilaize the database with those custom values + */ + @Override + protected void configure() { + addEnv("YCQL_KEYSPACE", keyspace); + addEnv("YCQL_USER", username); + addEnv("YCQL_PASSWORD", password); + } + + /** + * @param initScript path of the initialization script file + * @return {@link YugabyteYCQLContainer} instance + */ + public YugabyteYCQLContainer withInitScript(String initScript) { + this.initScript = initScript; + return this; + } + + /** + * Setting this would create the keyspace + * @param keyspace keyspace + * @return {@link YugabyteYCQLContainer} instance + */ + public YugabyteYCQLContainer withKeyspaceName(final String keyspace) { + this.keyspace = keyspace; + return this; + } + + /** + * Setting this would create the custom user role + * @param username user name + * @return {@link YugabyteYCQLContainer} instance + */ + public YugabyteYCQLContainer withUsername(final String username) { + this.username = username; + return this; + } + + /** + * Setting this along with {@link #withUsername(String)} would enable authentication + * @param password password + * @return {@link YugabyteYCQLContainer} instance + */ + public YugabyteYCQLContainer withPassword(final String password) { + this.password = password; + return this; + } + + /** + * Executes the initilization script + * @param containerInfo containerInfo + */ + @Override + protected void containerIsStarted(InspectContainerResponse containerInfo) { + if (initScript != null) { + ScriptUtils.runInitScript(new YugabyteYCQLDelegate(getSessionBuilder()), initScript); + } + } + + /** + * Builds a {@link CqlSession} instance + * @return {@link CqlSession} instance + */ + public CqlSession getSession() { + return getSessionBuilder().build(); + } + + /** + * Builder method for {#com.datastax.oss.driver.api.core.CqlSession} + * @return {@link CqlSessionBuilder} + */ + public CqlSessionBuilder getSessionBuilder() { + return CqlSession.builder().withLocalDatacenter(LOCAL_DC).withKeyspace(this.getKeyspace()) + .withAuthCredentials(this.getUsername(), this.getPassword()) + .addContactPoint(new InetSocketAddress(this.getHost(), this.getMappedPort(YCQL_PORT))); + } + + /** + * Username getter method + * @return username + */ + public String getUsername() { + return username; + } + + /** + * Password getter method + * @return password + */ + public String getPassword() { + return password; + } + + /** + * Keyspace getter method + * @return keyspace + */ + public String getKeyspace() { + return keyspace; + } + +} diff --git a/modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteYSQLContainer.java b/modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteYSQLContainer.java new file mode 100644 index 00000000000..5e7083856ee --- /dev/null +++ b/modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteYSQLContainer.java @@ -0,0 +1,137 @@ +package org.testcontainers.containers; + +import java.time.Duration; +import java.util.Set; + +import org.testcontainers.containers.strategy.YugabyteYSQLWaitStrategy; +import org.testcontainers.utility.DockerImageName; + +import static java.util.Collections.singleton; +import static org.testcontainers.containers.YugabyteContainerConstants.DEFAULT_IMAGE_NAME; +import static org.testcontainers.containers.YugabyteContainerConstants.ENTRYPOINT; +import static org.testcontainers.containers.YugabyteContainerConstants.JDBC_CONNECT_PREFIX; +import static org.testcontainers.containers.YugabyteContainerConstants.JDBC_DRIVER_CLASS; +import static org.testcontainers.containers.YugabyteContainerConstants.MASTER_DASHBOARD_PORT; +import static org.testcontainers.containers.YugabyteContainerConstants.TSERVER_DASHBOARD_PORT; +import static org.testcontainers.containers.YugabyteContainerConstants.YSQL_PORT; + +/** + * YugabyteDB YSQL (Structured Query Language) API container + * + * @author srinivasa-vasu + * @see YSQL API + */ + +public class YugabyteYSQLContainer extends JdbcDatabaseContainer { + + private String database = "yugabyte"; + + private String username = "yugabyte"; + + private String password = "yugabyte"; + + /** + * @param imageName image name + */ + public YugabyteYSQLContainer(final String imageName) { + this(DockerImageName.parse(imageName)); + } + + /** + * @param imageName image name + */ + public YugabyteYSQLContainer(final DockerImageName imageName) { + super(imageName); + imageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + withExposedPorts(YSQL_PORT, MASTER_DASHBOARD_PORT, TSERVER_DASHBOARD_PORT); + waitingFor(new YugabyteYSQLWaitStrategy(this).withStartupTimeout(Duration.ofSeconds(60))); + withCommand(ENTRYPOINT); + } + + @Override + public Set getLivenessCheckPortNumbers() { + return singleton(getMappedPort(YSQL_PORT)); + } + + /** + * Configures the environment variables. Setting up these variables would create the + * custom objects. Setting {@link #withDatabaseName(String)}, + * {@link #withUsername(String)}, {@link #withPassword(String)} these parameters will + * initilaize the database with those custom values + */ + + @Override + protected void configure() { + addEnv("YSQL_DB", database); + addEnv("YSQL_USER", username); + addEnv("YSQL_PASSWORD", password); + } + + @Override + public String getDriverClassName() { + return JDBC_DRIVER_CLASS; + } + + @Override + public String getJdbcUrl() { + return JDBC_CONNECT_PREFIX + "://" + getHost() + ":" + getMappedPort(YSQL_PORT) + "/" + database + + constructUrlParameters("?", "&"); + } + + @Override + public String getDatabaseName() { + return database; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getTestQueryString() { + return "SELECT 1"; + } + + /** + * Setting this would create the keyspace + * @param database database name + * @return {@link YugabyteYSQLContainer} instance + */ + + @Override + public YugabyteYSQLContainer withDatabaseName(final String database) { + this.database = database; + return this; + } + + /** + * Setting this would create the custom user role + * @param username user name + * @return {@link YugabyteYSQLContainer} instance + */ + + @Override + public YugabyteYSQLContainer withUsername(final String username) { + this.username = username; + return this; + } + + /** + * Setting this along with {@link #withUsername(String)} would enable authentication + * @param password password + * @return {@link YugabyteYSQLContainer} instance + */ + + @Override + public YugabyteYSQLContainer withPassword(final String password) { + this.password = password; + return this; + } + +} diff --git a/modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteYSQLContainerProvider.java b/modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteYSQLContainerProvider.java new file mode 100644 index 00000000000..3b729a5dc46 --- /dev/null +++ b/modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteYSQLContainerProvider.java @@ -0,0 +1,38 @@ +package org.testcontainers.containers; + +import org.testcontainers.jdbc.ConnectionUrl; + +import static org.testcontainers.containers.YugabyteContainerConstants.DEFAULT_IMAGE_NAME; +import static org.testcontainers.containers.YugabyteContainerConstants.DEFAULT_TAG; +import static org.testcontainers.containers.YugabyteContainerConstants.NAME; +import static org.testcontainers.containers.YugabyteContainerConstants.PASSWORD_PARAM; +import static org.testcontainers.containers.YugabyteContainerConstants.USER_PARAM; + +/** + * YugabyteDB YSQL (Structured Query Language) JDBC container provider + * + * @author srinivasa-vasu + */ +public class YugabyteYSQLContainerProvider extends JdbcDatabaseContainerProvider { + + @Override + public boolean supports(String databaseType) { + return databaseType.equals(NAME); + } + + @Override + public JdbcDatabaseContainer newInstance() { + return newInstance(DEFAULT_TAG); + } + + @Override + public JdbcDatabaseContainer newInstance(String tag) { + return new YugabyteYSQLContainer(DEFAULT_IMAGE_NAME.withTag(tag)); + } + + @Override + public JdbcDatabaseContainer newInstance(ConnectionUrl connectionUrl) { + return newInstanceFromConnectionUrl(connectionUrl, USER_PARAM, PASSWORD_PARAM); + } + +} diff --git a/modules/yugabytedb/src/main/java/org/testcontainers/containers/delegate/YugabyteYCQLDelegate.java b/modules/yugabytedb/src/main/java/org/testcontainers/containers/delegate/YugabyteYCQLDelegate.java new file mode 100644 index 00000000000..5a2eac05800 --- /dev/null +++ b/modules/yugabytedb/src/main/java/org/testcontainers/containers/delegate/YugabyteYCQLDelegate.java @@ -0,0 +1,41 @@ +package org.testcontainers.containers.delegate; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.CqlSessionBuilder; +import com.datastax.oss.driver.api.core.session.Session; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.testcontainers.containers.YugabyteYCQLContainer; +import org.testcontainers.delegate.AbstractDatabaseDelegate; + +/** + * Query execution delegate class for YCQL API to delegate init-scripts statements. + * + * @author srinivasa-vasu + * @see YugabyteYCQLContainer + */ +@Slf4j +@RequiredArgsConstructor +public final class YugabyteYCQLDelegate extends AbstractDatabaseDelegate { + + private final CqlSessionBuilder builder; + + @Override + protected Session createNewConnection() { + return builder.build(); + } + + @Override + public void execute(String statement, String scriptPath, int lineNumber, boolean continueOnError, + boolean ignoreFailedDrops) { + ((CqlSession) getConnection()).execute(statement); + } + + @Override + protected void closeConnectionQuietly(Session session) { + if (session != null) { + session.close(); + } + } + +} diff --git a/modules/yugabytedb/src/main/java/org/testcontainers/containers/strategy/YugabyteYCQLWaitStrategy.java b/modules/yugabytedb/src/main/java/org/testcontainers/containers/strategy/YugabyteYCQLWaitStrategy.java new file mode 100644 index 00000000000..de2b3c851b2 --- /dev/null +++ b/modules/yugabytedb/src/main/java/org/testcontainers/containers/strategy/YugabyteYCQLWaitStrategy.java @@ -0,0 +1,50 @@ +package org.testcontainers.containers.strategy; + +import java.util.concurrent.TimeUnit; + +import com.datastax.oss.driver.api.core.CqlSession; +import lombok.RequiredArgsConstructor; +import org.testcontainers.containers.YugabyteYCQLContainer; +import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; +import org.testcontainers.containers.wait.strategy.WaitStrategyTarget; + +import static org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess; +import static org.testcontainers.containers.YugabyteContainerConstants.YCQL_TEST_QUERY; + +/** + * Custom wait strategy for YCQL API. + * + *

+ * Though we can either use HTTP or PORT based wait strategy, when we create a custom + * keyspace/role, it gets executed asynchronously. As the wait on container.start() on a + * specific port wouldn't fully guarantee the custom object execution. It's better to + * check the DB status with this way with a smoke test query that uses the underlying + * custom objects and wait for the operation to complete. + *

+ * + * @author srinivasa-vasu + */ +@RequiredArgsConstructor +public final class YugabyteYCQLWaitStrategy extends AbstractWaitStrategy { + + private final WaitStrategyTarget target; + + @Override + public void waitUntilReady(WaitStrategyTarget target) { + YugabyteYCQLContainer container = (YugabyteYCQLContainer) target; + retryUntilSuccess((int) startupTimeout.getSeconds(), TimeUnit.SECONDS, () -> { + getRateLimiter().doWhenReady(() -> { + try (CqlSession session = container.getSession()) { + session.execute(YCQL_TEST_QUERY); + } + }); + return true; + }); + } + + @Override + public void waitUntilReady() { + waitUntilReady(target); + } + +} diff --git a/modules/yugabytedb/src/main/java/org/testcontainers/containers/strategy/YugabyteYSQLWaitStrategy.java b/modules/yugabytedb/src/main/java/org/testcontainers/containers/strategy/YugabyteYSQLWaitStrategy.java new file mode 100644 index 00000000000..1f114489f39 --- /dev/null +++ b/modules/yugabytedb/src/main/java/org/testcontainers/containers/strategy/YugabyteYSQLWaitStrategy.java @@ -0,0 +1,56 @@ +package org.testcontainers.containers.strategy; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.TimeUnit; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.testcontainers.containers.YugabyteYSQLContainer; +import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; +import org.testcontainers.containers.wait.strategy.WaitStrategyTarget; + +import static org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess; +import static org.testcontainers.containers.YugabyteContainerConstants.YSQL_TEST_QUERY; + +/** + * Custom wait strategy for YSQL API. + * + *

+ * Though we can either use HTTP or PORT based wait strategy, when we create a custom + * database/role, it gets executed asynchronously. As the wait on container.start() on a + * specific port wouldn't fully guarantee the custom object execution. It's better to + * check the DB status with this way with a smoke test query that uses the underlying + * custom objects and wait for the operation to complete. + *

+ * + * @author srinivasa-vasu + */ +@RequiredArgsConstructor +@Slf4j +public final class YugabyteYSQLWaitStrategy extends AbstractWaitStrategy { + + private final WaitStrategyTarget target; + + @Override + public void waitUntilReady(WaitStrategyTarget target) { + YugabyteYSQLContainer container = (YugabyteYSQLContainer) target; + retryUntilSuccess((int) startupTimeout.getSeconds(), TimeUnit.SECONDS, () -> { + getRateLimiter().doWhenReady(() -> { + try (Connection con = container.createConnection(container.getJdbcUrl())) { + con.createStatement().execute(YSQL_TEST_QUERY); + } + catch (SQLException ex) { + log.error("Error connecting to the database", ex); + } + }); + return true; + }); + } + + @Override + public void waitUntilReady() { + waitUntilReady(target); + } + +} diff --git a/modules/yugabytedb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider b/modules/yugabytedb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider new file mode 100644 index 00000000000..d4b25fba9f4 --- /dev/null +++ b/modules/yugabytedb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.containers.YugabyteYSQLContainerProvider diff --git a/modules/yugabytedb/src/test/java/org/testcontainers/YugabyteTestContainerConstants.java b/modules/yugabytedb/src/test/java/org/testcontainers/YugabyteTestContainerConstants.java new file mode 100644 index 00000000000..d94b01a422b --- /dev/null +++ b/modules/yugabytedb/src/test/java/org/testcontainers/YugabyteTestContainerConstants.java @@ -0,0 +1,18 @@ +package org.testcontainers; + +import org.testcontainers.utility.DockerImageName; + +/** + * @author srinivasa-vasu + */ +public interface YugabyteTestContainerConstants { + + String IMAGE_NAME = "yugabytedb/yugabyte:2.7.2.0-b216"; + + DockerImageName YBDB_TEST_IMAGE = DockerImageName.parse(IMAGE_NAME); + + String LOCAL_DC = "datacenter1"; + + int YCQL_PORT = 9042; + +} diff --git a/modules/yugabytedb/src/test/java/org/testcontainers/jdbc/yugabytedb/YugabyteYSQLJDBCDriverTest.java b/modules/yugabytedb/src/test/java/org/testcontainers/jdbc/yugabytedb/YugabyteYSQLJDBCDriverTest.java new file mode 100644 index 00000000000..510a71f26d3 --- /dev/null +++ b/modules/yugabytedb/src/test/java/org/testcontainers/jdbc/yugabytedb/YugabyteYSQLJDBCDriverTest.java @@ -0,0 +1,25 @@ +package org.testcontainers.jdbc.yugabytedb; + +import java.util.EnumSet; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.testcontainers.jdbc.AbstractJDBCDriverTest; + +import static java.util.Arrays.asList; + +/** + * YugabyteDB YSQL API JDBC connectivity driver test class + * + * @author srinivasa-vasu + */ +@RunWith(Parameterized.class) +public class YugabyteYSQLJDBCDriverTest extends AbstractJDBCDriverTest { + + @Parameterized.Parameters(name = "{index} - {0}") + public static Iterable data() { + return asList(new Object[][] { { "jdbc:tc:yugabyte://hostname/yugabyte?user=yugabyte&password=yugabyte", + EnumSet.noneOf(Options.class) }, }); + } + +} diff --git a/modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteYCQLUnitTest.java b/modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteYCQLUnitTest.java new file mode 100644 index 00000000000..ad3fc44f1aa --- /dev/null +++ b/modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteYCQLUnitTest.java @@ -0,0 +1,88 @@ +package org.testcontainers.junit.yugabytedb; + +import java.net.InetSocketAddress; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import org.junit.Test; +import org.testcontainers.containers.YugabyteYCQLContainer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.testcontainers.YugabyteTestContainerConstants.IMAGE_NAME; +import static org.testcontainers.YugabyteTestContainerConstants.LOCAL_DC; +import static org.testcontainers.YugabyteTestContainerConstants.YBDB_TEST_IMAGE; +import static org.testcontainers.YugabyteTestContainerConstants.YCQL_PORT; + +/** + * YugabyteDB YCQL API unit test class + * + * @author srinivasa-vasu + */ +public class YugabyteYCQLUnitTest { + + @Test + public void testSmoke() { + try (YugabyteYCQLContainer container = new YugabyteYCQLContainer(IMAGE_NAME)) { + container.start(); + assertNotNull("Smoke test simple query execution fails!", + performQuery(container, "SELECT release_version FROM system.local").one().getString(0)); + } + } + + @Test + public void testCustomKeyspace() throws InterruptedException { + String key = "random"; + try (YugabyteYCQLContainer container = new YugabyteYCQLContainer(YBDB_TEST_IMAGE).withKeyspaceName(key)) { + container.start(); + assertEquals("Custom keyspace creation fails!", key, + performQuery(container, + "SELECT keyspace_name FROM system_schema.keyspaces where keyspace_name='" + key + "'").one() + .getString(0)); + } + } + + @Test + public void testAuthenticationEnabled() throws InterruptedException { + String role = "random"; + try (YugabyteYCQLContainer container = new YugabyteYCQLContainer(YBDB_TEST_IMAGE).withUsername(role) + .withPassword(role)) { + container.start(); + assertEquals("Keyspace login fails with authentication enabled!", role, + performQuery(container, "SELECT role FROM system_auth.roles where role='" + role + "'").one() + .getString(0)); + } + } + + @Test + public void testAuthenticationDisabled() { + try (YugabyteYCQLContainer container = new YugabyteYCQLContainer(YBDB_TEST_IMAGE).withPassword("") + .withUsername("")) { + container.start(); + assertTrue("Query execution fails!", + performQuery(container, "SELECT release_version FROM system.local").wasApplied()); + } + } + + @Test + public void testInitScript() throws InterruptedException { + String key = "random"; + try (YugabyteYCQLContainer container = new YugabyteYCQLContainer(YBDB_TEST_IMAGE).withKeyspaceName(key) + .withUsername(key).withPassword(key).withInitScript("init/init_yql.sql")) { + container.start(); + assertTrue("Query execution fails to execute statements from a custom script!", + performQuery(container, "SELECT * FROM random.bar").wasApplied()); + } + } + + private ResultSet performQuery(YugabyteYCQLContainer container, String cql) { + try (CqlSession session = CqlSession.builder().withKeyspace(container.getKeyspace()) + .withAuthCredentials(container.getUsername(), container.getPassword()).withLocalDatacenter(LOCAL_DC) + .addContactPoint(new InetSocketAddress(container.getHost(), container.getMappedPort(YCQL_PORT))) + .build()) { + return session.execute(cql); + } + } + +} diff --git a/modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteYSQLUnitTest.java b/modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteYSQLUnitTest.java new file mode 100644 index 00000000000..707a3604991 --- /dev/null +++ b/modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteYSQLUnitTest.java @@ -0,0 +1,72 @@ +package org.testcontainers.junit.yugabytedb; + +import java.sql.SQLException; + +import org.junit.Test; +import org.testcontainers.containers.YugabyteYSQLContainer; +import org.testcontainers.db.AbstractContainerDatabaseTest; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.testcontainers.YugabyteTestContainerConstants.IMAGE_NAME; +import static org.testcontainers.YugabyteTestContainerConstants.YBDB_TEST_IMAGE; + +/** + * YugabyteDB YSQL API unit test class + * + * @author srinivasa-vasu + */ +public class YugabyteYSQLUnitTest extends AbstractContainerDatabaseTest { + + @Test + public void testSmoke() throws SQLException { + try (YugabyteYSQLContainer container = new YugabyteYSQLContainer(IMAGE_NAME)) { + container.start(); + assertEquals("Query execution fails!", 1, performQuery(container, "SELECT 1").getInt(1)); + } + } + + @Test + public void testCustomDatabase() throws SQLException { + String key = "random"; + try (YugabyteYSQLContainer container = new YugabyteYSQLContainer(YBDB_TEST_IMAGE).withDatabaseName(key)) { + container.start(); + assertEquals("Query execution on a custom database fails!", 1, + performQuery(container, "SELECT 1").getInt(1)); + } + } + + @Test + public void testExplicitInitScript() throws SQLException { + try (YugabyteYSQLContainer container = new YugabyteYSQLContainer(YBDB_TEST_IMAGE) + .withInitScript("init/init_yql.sql")) { + container.start(); + assertEquals("Value from the init script does not match the real value", "hello world", + performQuery(container, "SELECT foo FROM bar").getString(1)); + } + } + + @Test + public void testWithAdditionalUrlParamInJdbcUrl() { + try (YugabyteYSQLContainer container = new YugabyteYSQLContainer(YBDB_TEST_IMAGE) + .withUrlParam("sslmode", "disable").withUrlParam("application_name", "yugabyte")) { + container.start(); + String jdbcUrl = container.getJdbcUrl(); + assertThat(jdbcUrl, containsString("?")); + assertThat(jdbcUrl, containsString("&")); + assertThat(jdbcUrl, containsString("sslmode=disable")); + assertThat(jdbcUrl, containsString("application_name=yugabyte")); + } + } + + @Test + public void testWithCustomRole() throws SQLException { + try (YugabyteYSQLContainer container = new YugabyteYSQLContainer(YBDB_TEST_IMAGE).withDatabaseName("yugabyte") + .withPassword("yugabyte").withUsername("yugabyte")) { + container.start(); + assertEquals("Query execution with a custom role fails!", 1, performQuery(container, "SELECT 1").getInt(1)); + } + } + +} diff --git a/modules/yugabytedb/src/test/resources/init/init_yql.sql b/modules/yugabytedb/src/test/resources/init/init_yql.sql new file mode 100644 index 00000000000..f09647da277 --- /dev/null +++ b/modules/yugabytedb/src/test/resources/init/init_yql.sql @@ -0,0 +1,5 @@ +CREATE TABLE bar( + foo text primary key +); + +INSERT INTO bar (foo) VALUES ('hello world');