From af4c6b5c3b709022b7c1892dae71f965387f1c88 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 11 Apr 2023 10:49:57 +0200 Subject: [PATCH] Reinstate integration tests for R2DBC MySQL. Closes #1475 --- spring-data-r2dbc/pom.xml | 15 ++ .../dialect/DialectResolverUnitTests.java | 7 +- .../MySqlR2dbcRepositoryIntegrationTests.java | 95 ++++++++++ .../r2dbc/testing/MySqlDbTestSupport.java | 162 ++++++++++++++++++ 4 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/MySqlR2dbcRepositoryIntegrationTests.java create mode 100644 spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/testing/MySqlDbTestSupport.java diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index 9cd62795b..a2dbf6227 100644 --- a/spring-data-r2dbc/pom.xml +++ b/spring-data-r2dbc/pom.xml @@ -31,6 +31,7 @@ 1.0.0.RELEASE 1.1.3 1.0.0.RELEASE + 1.0.0 1.0.0 1.0.0.RELEASE 1.0.4 @@ -205,6 +206,13 @@ test + + mysql + mysql-connector-java + ${mysql-connector-java.version} + test + + com.oracle.database.jdbc ojdbc11 @@ -242,6 +250,13 @@ test + + io.asyncer + r2dbc-mysql + ${r2dbc-mysql.version} + test + + com.oracle.database.r2dbc oracle-r2dbc diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/dialect/DialectResolverUnitTests.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/dialect/DialectResolverUnitTests.java index 6efb1372d..5fffdd8bd 100644 --- a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/dialect/DialectResolverUnitTests.java +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/dialect/DialectResolverUnitTests.java @@ -2,6 +2,8 @@ package org.springframework.data.r2dbc.dialect; import static org.assertj.core.api.Assertions.*; +import io.asyncer.r2dbc.mysql.MySqlConnectionConfiguration; +import io.asyncer.r2dbc.mysql.MySqlConnectionFactory; import io.r2dbc.h2.H2ConnectionConfiguration; import io.r2dbc.h2.H2ConnectionFactory; import io.r2dbc.postgresql.PostgresqlConnectionConfiguration; @@ -30,7 +32,7 @@ import org.springframework.r2dbc.core.binding.BindMarkersFactory; */ public class DialectResolverUnitTests { - @Test // gh-20, gh-104 + @Test // GH-20, GH-104, GH-1475 void shouldResolveDatabaseType() { PostgresqlConnectionFactory postgres = new PostgresqlConnectionFactory(PostgresqlConnectionConfiguration.builder() @@ -38,10 +40,13 @@ public class DialectResolverUnitTests { H2ConnectionFactory h2 = new H2ConnectionFactory(H2ConnectionConfiguration.builder().inMemory("mem").build()); MariadbConnectionFactory mariadb = new MariadbConnectionFactory( MariadbConnectionConfiguration.builder().socket("/foo").username("bar").build()); + MySqlConnectionFactory mysql = MySqlConnectionFactory + .from(MySqlConnectionConfiguration.builder().host("foo").username("bar").build()); assertThat(DialectResolver.getDialect(postgres)).isEqualTo(PostgresDialect.INSTANCE); assertThat(DialectResolver.getDialect(h2)).isEqualTo(H2Dialect.INSTANCE); assertThat(DialectResolver.getDialect(mariadb)).isEqualTo(MySqlDialect.INSTANCE); + assertThat(DialectResolver.getDialect(mysql)).isEqualTo(MySqlDialect.INSTANCE); } @Test // gh-20, gh-104 diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/MySqlR2dbcRepositoryIntegrationTests.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/MySqlR2dbcRepositoryIntegrationTests.java new file mode 100644 index 000000000..2d539ce7a --- /dev/null +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/MySqlR2dbcRepositoryIntegrationTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2019-2023 the original author or authors. + * + * 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 + * + * https://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.springframework.data.r2dbc.repository; + +import io.r2dbc.spi.ConnectionFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration; +import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; +import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory; +import org.springframework.data.r2dbc.testing.ExternalDatabase; +import org.springframework.data.r2dbc.testing.MySqlDbTestSupport; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * Integration tests for {@link LegoSetRepository} using {@link R2dbcRepositoryFactory} against MySQL. + * + * @author Mark Paluch + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration +public class MySqlR2dbcRepositoryIntegrationTests extends AbstractR2dbcRepositoryIntegrationTests { + + @RegisterExtension public static final ExternalDatabase database = MySqlDbTestSupport.database(); + + @Configuration + @EnableR2dbcRepositories(considerNestedRepositories = true, + includeFilters = @Filter(classes = MySqlLegoSetRepository.class, type = FilterType.ASSIGNABLE_TYPE)) + static class IntegrationTestConfiguration extends AbstractR2dbcConfiguration { + + @Bean + @Override + public ConnectionFactory connectionFactory() { + return MySqlDbTestSupport.createConnectionFactory(database); + } + } + + @Override + protected DataSource createDataSource() { + return MySqlDbTestSupport.createDataSource(database); + } + + @Override + protected ConnectionFactory createConnectionFactory() { + return MySqlDbTestSupport.createConnectionFactory(database); + } + + @Override + protected String getCreateTableStatement() { + return MySqlDbTestSupport.CREATE_TABLE_LEGOSET_WITH_ID_GENERATION; + } + + @Override + protected Class getRepositoryInterfaceType() { + return MySqlLegoSetRepository.class; + } + + interface MySqlLegoSetRepository extends LegoSetRepository { + + @Override + @Query("SELECT name FROM legoset") + Flux findAsProjection(); + + @Override + @Query("SELECT * FROM legoset WHERE manual = :manual") + Mono findByManual(int manual); + + @Override + @Query("SELECT id FROM legoset") + Flux findAllIds(); + } +} diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/testing/MySqlDbTestSupport.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/testing/MySqlDbTestSupport.java new file mode 100644 index 000000000..f988967ab --- /dev/null +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/testing/MySqlDbTestSupport.java @@ -0,0 +1,162 @@ +/* + * Copyright 2019-2023 the original author or authors. + * + * 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 + * + * https://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.springframework.data.r2dbc.testing; + +import io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; +import lombok.SneakyThrows; + +import java.util.function.Supplier; +import java.util.stream.Stream; + +import javax.sql.DataSource; + +import org.springframework.data.r2dbc.testing.ExternalDatabase.ProvidedDatabase; +import org.testcontainers.containers.MySQLContainer; + +import com.mysql.cj.jdbc.MysqlDataSource; + +/** + * Utility class for testing against MySQL. + * + * @author Mark Paluch + * @author Jens Schauder + */ +public class MySqlDbTestSupport { + + private static ExternalDatabase testContainerDatabase; + + public static final String CREATE_TABLE_LEGOSET = "CREATE TABLE legoset (\n" // + + " id integer PRIMARY KEY,\n" // + + " name varchar(255) NOT NULL,\n" // + + " manual integer NULL\n," // + + " cert varbinary(255) NULL\n" // + + ") ENGINE=InnoDB;"; + + public static final String CREATE_TABLE_LEGOSET_WITH_ID_GENERATION = "CREATE TABLE legoset (\n" // + + " id integer AUTO_INCREMENT PRIMARY KEY,\n" // + + " name varchar(255) NOT NULL,\n" // + + " flag boolean NOT NULL,\n" // + + " manual integer NULL\n" // + + ") ENGINE=InnoDB;"; + + public static final String CREATE_TABLE_LEGOSET_WITH_MIXED_CASE_NAMES = "CREATE TABLE `LegoSet` (\n" // + + " `Id` integer AUTO_INCREMENT PRIMARY KEY,\n" // + + " `Name` varchar(255) NOT NULL,\n" // + + " `Manual` integer NULL\n" // + + ") ENGINE=InnoDB;"; + + public static final String DROP_TABLE_LEGOSET_WITH_MIXED_CASE_NAMES = "DROP TABLE `LegoSet`"; + + /** + * Returns a database either hosted locally at {@code localhost:3306/mysql} or running inside Docker. + * + * @return information about the database. Guaranteed to be not {@literal null}. + */ + public static ExternalDatabase database() { + + if (Boolean.getBoolean("spring.data.r2dbc.test.preferLocalDatabase")) { + + return getFirstWorkingDatabase( // + MySqlDbTestSupport::local, // + MySqlDbTestSupport::testContainer // + ); + } else { + + return getFirstWorkingDatabase( // + MySqlDbTestSupport::testContainer, // + MySqlDbTestSupport::local // + ); + } + } + + @SafeVarargs + private static ExternalDatabase getFirstWorkingDatabase(Supplier... suppliers) { + + return Stream.of(suppliers).map(Supplier::get) // + .filter(ExternalDatabase::checkValidity) // + .findFirst() // + .orElse(ExternalDatabase.unavailable()); + } + + /** + * Returns a locally provided database . + */ + private static ExternalDatabase local() { + + return ProvidedDatabase.builder() // + .hostname("localhost") // + .port(3306) // + .database("mysql") // + .username("root") // + .password("my-secret-pw") // + .jdbcUrl("jdbc:mysql://localhost:3306/mysql") // + .build(); + } + + /** + * Returns a database provided via Testcontainers. + */ + private static ExternalDatabase testContainer() { + + if (testContainerDatabase == null) { + + try { + + var container = new MySQLContainer<>("mysql:8.0.32").withUsername("test").withPassword("test") + .withConfigurationOverride(""); + + container.start(); + + testContainerDatabase = ProvidedDatabase.builder(container) // + .username("root") // + .database(container.getDatabaseName()) // + .build(); + } catch (IllegalStateException ise) { + // docker not available. + testContainerDatabase = ExternalDatabase.unavailable(); + } + } + + return testContainerDatabase; + } + + /** + * Creates a new R2DBC MySQL {@link ConnectionFactory} configured from the {@link ExternalDatabase}. + */ + public static ConnectionFactory createConnectionFactory(ExternalDatabase database) { + + ConnectionFactoryOptions options = ConnectionUtils.createOptions("mysql", database); + return new MySqlConnectionFactoryProvider().create(options); + } + + /** + * Creates a new {@link DataSource} configured from the {@link ExternalDatabase}. + */ + @SneakyThrows + public static DataSource createDataSource(ExternalDatabase database) { + + MysqlDataSource dataSource = new MysqlDataSource(); + + dataSource.setUser(database.getUsername()); + dataSource.setPassword(database.getPassword()); + dataSource.setUrl( + String.format("jdbc:mysql://%s:%d/%s?", database.getHostname(), database.getPort(), database.getDatabase())); + + return dataSource; + } +}