From 76b2107e2a328947879fefae9415745b73791525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kraus?= Date: Fri, 22 Sep 2023 16:08:31 +0200 Subject: [PATCH] Mysql tests with testcontainers. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomáš Kraus --- .../helidon/dbclient/jdbc/JdbcStatement.java | 8 +- .../jdbc/JdbcTransactionStatement.java | 11 +- .../{jdbc => h2}/H2SetupProvider.java | 2 +- .../h2/src/main/java/module-info.java | 2 +- .../dbclient/h2/src/test/resources/h2.yaml | 2 +- tests/integration/dbclient/mysql/pom.xml | 179 +++++++++++++++++ .../dbclient/mysql/MySqlSetupProvider.java | 189 ++++++++++++++++++ .../mysql/src/main/java/module-info.java | 35 ++++ .../mysql/src/test/resources/mysql.yaml | 70 +++++++ tests/integration/dbclient/pom.xml | 1 + 10 files changed, 494 insertions(+), 5 deletions(-) rename tests/integration/dbclient/h2/src/main/java/io/helidon/tests/integration/dbclient/{jdbc => h2}/H2SetupProvider.java (98%) create mode 100644 tests/integration/dbclient/mysql/pom.xml create mode 100644 tests/integration/dbclient/mysql/src/main/java/io/helidon/tests/integration/dbclient/mysql/MySqlSetupProvider.java create mode 100644 tests/integration/dbclient/mysql/src/main/java/module-info.java create mode 100644 tests/integration/dbclient/mysql/src/test/resources/mysql.yaml diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatement.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatement.java index 4a437e76332..e1cc63bc738 100644 --- a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatement.java +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcStatement.java @@ -108,7 +108,13 @@ protected PreparedStatement prepareStatement(DbClientServiceContext serviceConte * @return statement */ protected PreparedStatement prepareStatement(String stmtName, String stmt) { - return prepareStatement(connectionPool.connection(), stmtName, stmt); + Connection connection = connectionPool.connection(); + try { + connection.setAutoCommit(true); + } catch (SQLException e) { + throw new DbClientException("Failed to set autocommit to true", e); + } + return prepareStatement(connection, stmtName, stmt); } /** diff --git a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcTransactionStatement.java b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcTransactionStatement.java index 9ff92fcefc3..c584fee0a68 100644 --- a/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcTransactionStatement.java +++ b/dbclient/jdbc/src/main/java/io/helidon/dbclient/jdbc/JdbcTransactionStatement.java @@ -15,8 +15,11 @@ */ package io.helidon.dbclient.jdbc; +import java.sql.Connection; import java.sql.PreparedStatement; +import java.sql.SQLException; +import io.helidon.dbclient.DbClientException; import io.helidon.dbclient.DbStatement; /** @@ -44,6 +47,12 @@ protected JdbcTransactionStatement(JdbcConnectionPool connectionPool, @Override protected PreparedStatement prepareStatement(String stmtName, String stmt) { - return prepareStatement(transactionContext.connection(), stmtName, stmt); + Connection connection = transactionContext.connection(); + try { + connection.setAutoCommit(false); + } catch (SQLException e) { + throw new DbClientException("Failed to set autocommit to false", e); + } + return prepareStatement(connection, stmtName, stmt); } } diff --git a/tests/integration/dbclient/h2/src/main/java/io/helidon/tests/integration/dbclient/jdbc/H2SetupProvider.java b/tests/integration/dbclient/h2/src/main/java/io/helidon/tests/integration/dbclient/h2/H2SetupProvider.java similarity index 98% rename from tests/integration/dbclient/h2/src/main/java/io/helidon/tests/integration/dbclient/jdbc/H2SetupProvider.java rename to tests/integration/dbclient/h2/src/main/java/io/helidon/tests/integration/dbclient/h2/H2SetupProvider.java index ddb6508c451..e63c1720fc4 100644 --- a/tests/integration/dbclient/h2/src/main/java/io/helidon/tests/integration/dbclient/jdbc/H2SetupProvider.java +++ b/tests/integration/dbclient/h2/src/main/java/io/helidon/tests/integration/dbclient/h2/H2SetupProvider.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.tests.integration.dbclient.jdbc; +package io.helidon.tests.integration.dbclient.h2; import java.nio.file.Paths; import java.sql.SQLException; diff --git a/tests/integration/dbclient/h2/src/main/java/module-info.java b/tests/integration/dbclient/h2/src/main/java/module-info.java index bec50ffe7ec..173de57dac4 100644 --- a/tests/integration/dbclient/h2/src/main/java/module-info.java +++ b/tests/integration/dbclient/h2/src/main/java/module-info.java @@ -27,6 +27,6 @@ requires io.helidon.tests.integration.dbclient.common; provides io.helidon.tests.integration.dbclient.common.spi.SetupProvider - with io.helidon.tests.integration.dbclient.jdbc.H2SetupProvider; + with io.helidon.tests.integration.dbclient.h2.H2SetupProvider; } \ No newline at end of file diff --git a/tests/integration/dbclient/h2/src/test/resources/h2.yaml b/tests/integration/dbclient/h2/src/test/resources/h2.yaml index 368d3227eee..7945968a394 100644 --- a/tests/integration/dbclient/h2/src/test/resources/h2.yaml +++ b/tests/integration/dbclient/h2/src/test/resources/h2.yaml @@ -20,7 +20,7 @@ db: password: "password" database: "test" connection: - url: jdbc:h2:tcp://${db.host}:${db.port}/${db.database};DATABASE_TO_UPPER=FALSE + url: jdbc:h2:tcp://localhost:9092/test;DATABASE_TO_UPPER=FALSE health-check: type: query statement: "SELECT 0" diff --git a/tests/integration/dbclient/mysql/pom.xml b/tests/integration/dbclient/mysql/pom.xml new file mode 100644 index 00000000000..c08a01d9344 --- /dev/null +++ b/tests/integration/dbclient/mysql/pom.xml @@ -0,0 +1,179 @@ + + + + + 4.0.0 + + io.helidon.tests.integration.dbclient + helidon-tests-integration-dbclient-project + 4.0.0-SNAPSHOT + ../pom.xml + + helidon-tests-integration-dbclient-mysql + Helidon Tests Integration Database Client MySQL + + + false + + + + + + org.testcontainers + testcontainers-bom + 1.19.0 + pom + import + + + + + + + io.helidon.tests.integration.dbclient + helidon-tests-integration-dbclient-common + ${project.version} + + + io.helidon.integrations.db + helidon-integrations-db-mysql + ${project.version} + + + mysql + mysql-connector-java + test + + + io.helidon.config + helidon-config + + + io.helidon.config + helidon-config-yaml + + + io.helidon.dbclient + helidon-dbclient + + + io.helidon.common + helidon-common-mapper + + + io.helidon.dbclient + helidon-dbclient-jdbc + test + + + io.helidon.dbclient + helidon-dbclient-hikari + test + + + org.slf4j + slf4j-jdk14 + test + + + org.junit.platform + junit-platform-launcher + + + org.junit.jupiter + junit-jupiter-engine + + + org.junit.jupiter + junit-jupiter-api + test + + + io.helidon.tests.integration + helidon-tests-integration-harness + ${project.version} + test + + + org.hamcrest + hamcrest-all + test + + + org.testcontainers + mysql + + + org.testcontainers + jdbc + + + org.testcontainers + testcontainers + + + + + + + src/test/resources + true + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*IT + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + ${maven.test.redirectTestOutputToFile} + methods + 10 + + true + + + io.helidon.tests.integration.dbclient:helidon-tests-integration-dbclient-common + + + io.helidon.tests.integration.dbclient.common.tests.*IT + + + + + test + integration-test + + integration-test + + + + + + + + diff --git a/tests/integration/dbclient/mysql/src/main/java/io/helidon/tests/integration/dbclient/mysql/MySqlSetupProvider.java b/tests/integration/dbclient/mysql/src/main/java/io/helidon/tests/integration/dbclient/mysql/MySqlSetupProvider.java new file mode 100644 index 00000000000..476bd7f0f93 --- /dev/null +++ b/tests/integration/dbclient/mysql/src/main/java/io/helidon/tests/integration/dbclient/mysql/MySqlSetupProvider.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.tests.integration.dbclient.mysql; + +import java.net.URI; +import java.sql.DriverManager; +import java.util.Map; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.dbclient.DbClient; +import io.helidon.dbclient.DbExecute; +import io.helidon.tests.integration.dbclient.common.model.Pokemon; +import io.helidon.tests.integration.dbclient.common.model.Type; +import io.helidon.tests.integration.dbclient.common.spi.SetupProvider; +import io.helidon.tests.integration.dbclient.common.tests.MapperIT; + +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestPlan; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; +//import org.testcontainers.containers.MySQLContainer; + +/** + * MySQL database setup. + * Provides tests {@link io.helidon.config.Config} and {@link io.helidon.dbclient.DbClient} instance for junit tests. + * Starts and initializes H2 database. + */ +public class MySqlSetupProvider implements SetupProvider, TestExecutionListener { + + private static final System.Logger LOGGER = System.getLogger(MapperIT.class.getName()); + private static final Config CONFIG = initConfig(); + private static final String CONTAINER = CONFIG.get("db.container").asString().get(); + private static final String ROOT_PW = CONFIG.get("db.rootpw").asString().get(); + private static final String USER = CONFIG.get("db.connection.username").asString().get(); + private static final String PASSWORD = CONFIG.get("db.connection.password").asString().get(); + private static final String URL = CONFIG.get("db.connection.url").asString().get(); + private static final int TIMEOUT = 60; + + private static DbClient DB_CLIENT = null; + private static Config DB_CONFIG = null; + + private static Config initConfig() { + return Config.create(ConfigSources.classpath("mysql.yaml")); + } + + public MySqlSetupProvider() { + } + + @Override + public Config config() { + return DB_CONFIG; + } + + @Override + public DbClient dbClient() { + return DB_CLIENT; + } + + private GenericContainer container = null; + + /** @noinspection resource*/ + public void testPlanExecutionStarted(TestPlan testPlan) { + System.out.printf("Starting %s%n", CONTAINER); + URI uri = uriFromDbUrl(URL); + int port = uri.getPort(); + String database = dbNameFromUriPath(uri.getPath()); + container = new GenericContainer<>(DockerImageName.parse(CONTAINER)) + .withEnv("MYSQL_ROOT_PASSWORD", ROOT_PW) + .withEnv("MYSQL_USER", USER) + .withEnv("MYSQL_PASSWORD", PASSWORD) + .withEnv("MYSQL_DATABASE", database) + .withExposedPorts(port); + container.start(); + //String host = container.getHost(); + int dbPort = container.getMappedPort(port); + DB_CONFIG = Config.create( + ConfigSources.create(Map.of("db.connection.url", CONFIG.get("db.connection.url") + .as(String.class) + .get() + .replaceFirst(Integer.toString(port), Integer.toString(dbPort)))), + ConfigSources.classpath("mysql.yaml")); + waitForStart(DB_CONFIG.get("db.connection.url").as(String.class).get(), USER, PASSWORD); + DB_CLIENT = DbClient.builder(DB_CONFIG.get("db")).build(); + initSchema(DB_CLIENT); + initData(DB_CLIENT); + } + + public void testPlanExecutionFinished(TestPlan testPlan) { + System.out.printf("Stopping %s%n", CONTAINER); + dropSchema(DB_CLIENT); + container.stop(); + } + + // jdbc:mysql://127.0.0.1:3306/database is not valid URI. + // Only substring following "jdbc:" must be passed to URI factory method + private URI uriFromDbUrl(String url) { + int separator = url.indexOf(':'); // 4 + if (separator == -1) { + throw new IllegalArgumentException("Missing ':' character to separate leading jdbc prefix in database URL"); + } + if (url.length() < separator + 2) { + throw new IllegalArgumentException("Missing characters after \"jdbc:\"prefix"); + } + return URI.create(url.substring(separator + 1)); + } + + // Remove leading '/' character from URI path and return it as database name + private String dbNameFromUriPath(String uriPath) { + if (uriPath.length() == 0) { + throw new IllegalArgumentException("Database name is empty"); + } + String dbName = uriPath.charAt(0) == '/' ? uriPath.substring(1, uriPath.length()) : uriPath; + if (dbName.length() == 0) { + throw new IllegalArgumentException("Database name is empty"); + } + return dbName; + } + + private static void waitForStart(String url, String user, String password) { + System.out.printf("Waiting for %s to come up%n", CONTAINER); + long endTm = 1000 * TIMEOUT + System.currentTimeMillis(); + DriverManager.setLoginTimeout(2); + while (true) { + try { + DriverManager.getConnection(url, user, password); + break; + } catch (Throwable th) { + if (System.currentTimeMillis() > endTm) { + throw new IllegalStateException("Database startup failed!", th); + } + LOGGER.log(System.Logger.Level.DEBUG, () -> String.format("Exception: %s", th.getMessage()), th); + try { + Thread.sleep(1000); + } catch (InterruptedException ignored) { + } + } + } + } + + private static void initSchema(DbClient dbClient) { + DbExecute exec = dbClient.execute(); + exec.namedDml("create-types"); + exec.namedDml("create-pokemons"); + exec.namedDml("create-poketypes"); + } + + private static void initData(DbClient dbClient) { + DbExecute exec = dbClient.execute(); + long count = 0; + for (Map.Entry entry : Type.TYPES.entrySet()) { + count += exec.namedInsert("insert-type", entry.getKey(), entry.getValue().name()); + } + + for (Map.Entry entry : Pokemon.POKEMONS.entrySet()) { + count += exec.namedInsert("insert-pokemon", entry.getKey(), entry.getValue().getName()); + } + + for (Map.Entry entry : Pokemon.POKEMONS.entrySet()) { + Pokemon pokemon = entry.getValue(); + for (Type type : pokemon.getTypes()) { + count += exec.namedInsert("insert-poketype", pokemon.getId(), type.id()); + } + } + LOGGER.log(System.Logger.Level.INFO, String.format("executed %s statements", count)); + } + + private static void dropSchema(DbClient dbClient) { + DbExecute exec = dbClient.execute(); + exec.namedDml("drop-poketypes"); + exec.namedDml("drop-pokemons"); + exec.namedDml("drop-types"); + } + + +} diff --git a/tests/integration/dbclient/mysql/src/main/java/module-info.java b/tests/integration/dbclient/mysql/src/main/java/module-info.java new file mode 100644 index 00000000000..6ecbcdc4810 --- /dev/null +++ b/tests/integration/dbclient/mysql/src/main/java/module-info.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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. + */ + +/** + * Helidon Database Client Integration Tests with MySQL Database. + */ +module helidon.tests.integration.dbclient.mysql { + + requires io.helidon.config; + requires io.helidon.dbclient; + requires io.helidon.tests.integration.dbclient.common; + requires org.junit.platform.launcher; + requires java.sql; + //requires mysql; + requires testcontainers; + + provides io.helidon.tests.integration.dbclient.common.spi.SetupProvider + with io.helidon.tests.integration.dbclient.mysql.MySqlSetupProvider; + provides org.junit.platform.launcher.TestExecutionListener + with io.helidon.tests.integration.dbclient.mysql.MySqlSetupProvider; + +} \ No newline at end of file diff --git a/tests/integration/dbclient/mysql/src/test/resources/mysql.yaml b/tests/integration/dbclient/mysql/src/test/resources/mysql.yaml new file mode 100644 index 00000000000..35bbd484495 --- /dev/null +++ b/tests/integration/dbclient/mysql/src/test/resources/mysql.yaml @@ -0,0 +1,70 @@ +# +# Copyright (c) 2019, 2023 Oracle and/or its affiliates. +# +# Licensed 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. +# + +db: + container: mysql:8.0 + source: jdbc + rootpw: r00t_p4ssw0rd + connection: + url: jdbc:mysql://127.0.0.1:3306/pokemon?useSSL=false&allowPublicKeyRetrieval=true + username: user + password: p4ssw0rd + health-check: + type: query + statement: "SELECT 0" + statements: + ping: "SELECT 0" + ping-query: "SELECT 0" + create-types: "CREATE TABLE Types (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(64) NOT NULL)" + create-pokemons: "CREATE TABLE Pokemons (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(64) NOT NULL)" + create-poketypes: "CREATE TABLE PokemonTypes (id_pokemon INTEGER NOT NULL REFERENCES Pokemons(id), id_type INTEGER NOT NULL REFERENCES Types(id))" + drop-types: "DROP TABLE Types" + drop-pokemons: "DROP TABLE Pokemons" + drop-poketypes: "DROP TABLE PokemonTypes" + insert-type: "INSERT INTO Types(id, name) VALUES(?, ?)" + insert-pokemon: "INSERT INTO Pokemons(id, name) VALUES(?, ?)" + insert-poketype: "INSERT INTO PokemonTypes(id_pokemon, id_type) VALUES(?, ?)" + select-types: "SELECT id, name FROM Types" + select-pokemons: "SELECT id, name FROM Pokemons" + select-poketypes: "SELECT id_pokemon, id_type FROM PokemonTypes p WHERE id_pokemon = ?" + select-poketypes-all: "SELECT id_pokemon, id_type FROM PokemonTypes" + select-max-id: "SELECT MAX(id) FROM Pokemons" + select-pokemon-named-arg: "SELECT id, name FROM Pokemons WHERE name=:name" + select-pokemon-order-arg: "SELECT id, name FROM Pokemons WHERE name=?" + insert-pokemon-named-arg: "INSERT INTO Pokemons(id, name) VALUES(:id, :name)" + insert-pokemon-order-arg: "INSERT INTO Pokemons(id, name) VALUES(?, ?)" + insert-pokemon-order-arg-rev: "INSERT INTO Pokemons(name, id) VALUES(?, ?)" + select-pokemon-by-id: "SELECT id, name FROM Pokemons WHERE id=?" + update-pokemon-named-arg: "UPDATE Pokemons SET name=:name WHERE id=:id" + update-pokemon-order-arg: "UPDATE Pokemons SET name=? WHERE id=?" + delete-pokemon-named-arg: "DELETE FROM Pokemons WHERE id=:id" + delete-pokemon-order-arg: "DELETE FROM Pokemons WHERE id=?" + delete-pokemon-full-named-arg: "DELETE FROM Pokemons WHERE name=:name AND id=:id" + delete-pokemon-full-order-arg: "DELETE FROM Pokemons WHERE name=? AND id=?" + select-pokemons-idrng-named-arg: "SELECT id, name FROM Pokemons WHERE id > :idmin AND id < :idmax" + select-pokemons-idrng-order-arg: "SELECT id, name FROM Pokemons WHERE id > ? AND id < ?" + select-pokemons-error-arg: "SELECT id, name FROM Pokemons WHERE id > :id AND name = ?" + +test: + ping-dml: false + +health-check: + noDetails: + health: + endpoint: healthNoDetails + details: + health: + endpoint: healthDetails diff --git a/tests/integration/dbclient/pom.xml b/tests/integration/dbclient/pom.xml index b9374f15b78..2d0645a2379 100644 --- a/tests/integration/dbclient/pom.xml +++ b/tests/integration/dbclient/pom.xml @@ -35,6 +35,7 @@ common h2 + mysql app