diff --git a/spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java index 609f8e5bf125..ef922909523f 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -57,22 +57,43 @@ void runWithBitnamiImageCreatesConnectionDetails(JdbcConnectionDetails connectio assertConnectionDetails(connectionDetails); } + @DockerComposeTest(composeFile = "postgres-application-name-compose.yaml", image = TestImage.POSTGRESQL) + void runCreatesConnectionDetailsApplicationName(JdbcConnectionDetails connectionDetails) + throws ClassNotFoundException { + assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:postgresql://") + .endsWith("?ApplicationName=spring+boot"); + checkApplicationName(connectionDetails, "spring boot"); + } + private void assertConnectionDetails(JdbcConnectionDetails connectionDetails) { assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); assertThat(connectionDetails.getPassword()).isEqualTo("secret"); assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:postgresql://").endsWith("/mydatabase"); } - @SuppressWarnings("unchecked") private void checkDatabaseAccess(JdbcConnectionDetails connectionDetails) throws ClassNotFoundException { + assertThat(queryForObject(connectionDetails, DatabaseDriver.POSTGRESQL.getValidationQuery(), Integer.class)) + .isEqualTo(1); + } + + private void checkApplicationName(JdbcConnectionDetails connectionDetails, String applicationName) + throws ClassNotFoundException { + assertThat(queryForObject(connectionDetails, "select current_setting('application_name')", String.class)) + .isEqualTo(applicationName); + } + + @SuppressWarnings("unchecked") + private T queryForObject(JdbcConnectionDetails connectionDetails, String sql, Class result) + throws ClassNotFoundException { SimpleDriverDataSource dataSource = new SimpleDriverDataSource(); dataSource.setUrl(connectionDetails.getJdbcUrl()); dataSource.setUsername(connectionDetails.getUsername()); dataSource.setPassword(connectionDetails.getPassword()); dataSource.setDriverClass((Class) ClassUtils.forName(connectionDetails.getDriverClassName(), getClass().getClassLoader())); - JdbcTemplate template = new JdbcTemplate(dataSource); - assertThat(template.queryForObject(DatabaseDriver.POSTGRESQL.getValidationQuery(), Integer.class)).isEqualTo(1); + return new JdbcTemplate(dataSource).queryForObject(sql, result); } } diff --git a/spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java index dab0edc7cf3e..c415217ac626 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -20,6 +20,7 @@ import io.r2dbc.spi.ConnectionFactories; import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.Option; import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails; import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; @@ -60,21 +61,42 @@ void runWithBitnamiImageCreatesConnectionDetails(R2dbcConnectionDetails connecti assertConnectionDetails(connectionDetails); } + @DockerComposeTest(composeFile = "postgres-application-name-compose.yaml", image = TestImage.POSTGRESQL) + void runCreatesConnectionDetailsApplicationName(R2dbcConnectionDetails connectionDetails) { + assertConnectionDetails(connectionDetails); + ConnectionFactoryOptions options = connectionDetails.getConnectionFactoryOptions(); + assertThat(options.getValue(Option.valueOf("applicationName"))).isEqualTo("spring boot"); + checkApplicationName(connectionDetails, "spring boot"); + } + private void assertConnectionDetails(R2dbcConnectionDetails connectionDetails) { - ConnectionFactoryOptions connectionFactoryOptions = connectionDetails.getConnectionFactoryOptions(); - assertThat(connectionFactoryOptions.toString()).contains("database=mydatabase", "driver=postgresql", - "password=REDACTED", "user=myuser"); - assertThat(connectionFactoryOptions.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("secret"); + ConnectionFactoryOptions options = connectionDetails.getConnectionFactoryOptions(); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.HOST)).isNotNull(); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.PORT)).isNotNull(); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("mydatabase"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.USER)).isEqualTo("myuser"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("secret"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("postgresql"); } private void checkDatabaseAccess(R2dbcConnectionDetails connectionDetails) { + Integer result = queryForObject(connectionDetails, DatabaseDriver.POSTGRESQL.getValidationQuery(), + Integer.class); + assertThat(result).isEqualTo(1); + } + + private void checkApplicationName(R2dbcConnectionDetails connectionDetails, String applicationName) { + assertThat(queryForObject(connectionDetails, "select current_setting('application_name')", String.class)) + .isEqualTo(applicationName); + } + + private T queryForObject(R2dbcConnectionDetails connectionDetails, String sql, Class result) { ConnectionFactoryOptions connectionFactoryOptions = connectionDetails.getConnectionFactoryOptions(); - Object result = DatabaseClient.create(ConnectionFactories.get(connectionFactoryOptions)) - .sql(DatabaseDriver.POSTGRESQL.getValidationQuery()) - .map((row, metadata) -> row.get(0)) + return DatabaseClient.create(ConnectionFactories.get(connectionFactoryOptions)) + .sql(sql) + .mapValue(result) .first() .block(Duration.ofSeconds(30)); - assertThat(result).isEqualTo(1); } } diff --git a/spring-boot-project/spring-boot-docker-compose/src/dockerTest/resources/org/springframework/boot/docker/compose/service/connection/postgres/postgres-application-name-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/dockerTest/resources/org/springframework/boot/docker/compose/service/connection/postgres/postgres-application-name-compose.yaml new file mode 100644 index 000000000000..2b7721344111 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/dockerTest/resources/org/springframework/boot/docker/compose/service/connection/postgres/postgres-application-name-compose.yaml @@ -0,0 +1,12 @@ +services: + database: + image: '{imageName}' + ports: + - '5432' + environment: + - 'POSTGRES_USER=myuser' + - 'POSTGRES_DB=mydatabase' + - 'POSTGRES_PASSWORD=secret' + labels: + org.springframework.boot.jdbc.parameters: 'ApplicationName=spring+boot' + org.springframework.boot.r2dbc.parameters: 'applicationName=spring boot' diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionSource.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionSource.java index 4ddcc877612c..e3087c71428d 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionSource.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionSource.java @@ -17,6 +17,7 @@ package org.springframework.boot.docker.compose.service.connection; import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.core.env.Environment; /** * Passed to {@link DockerComposeConnectionDetailsFactory} to provide details of the @@ -32,12 +33,16 @@ public final class DockerComposeConnectionSource { private final RunningService runningService; + private final Environment environment; + /** * Create a new {@link DockerComposeConnectionSource} instance. * @param runningService the running Docker Compose service + * @param environment environment in which the current application is running */ - DockerComposeConnectionSource(RunningService runningService) { + DockerComposeConnectionSource(RunningService runningService, Environment environment) { this.runningService = runningService; + this.environment = environment; } /** @@ -48,4 +53,13 @@ public RunningService getRunningService() { return this.runningService; } + /** + * Environment in which the current application is running. + * @return the environment + * @since 3.5.0 + */ + public Environment getEnvironment() { + return this.environment; + } + } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeServiceConnectionsApplicationListener.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeServiceConnectionsApplicationListener.java index 4cf5135d26ed..662e08276bfc 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeServiceConnectionsApplicationListener.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeServiceConnectionsApplicationListener.java @@ -31,6 +31,7 @@ import org.springframework.boot.docker.compose.lifecycle.DockerComposeServicesReadyEvent; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; +import org.springframework.core.env.Environment; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -59,13 +60,15 @@ class DockerComposeServiceConnectionsApplicationListener public void onApplicationEvent(DockerComposeServicesReadyEvent event) { ApplicationContext applicationContext = event.getSource(); if (applicationContext instanceof BeanDefinitionRegistry registry) { - registerConnectionDetails(registry, event.getRunningServices()); + Environment environment = applicationContext.getEnvironment(); + registerConnectionDetails(registry, environment, event.getRunningServices()); } } - private void registerConnectionDetails(BeanDefinitionRegistry registry, List runningServices) { + private void registerConnectionDetails(BeanDefinitionRegistry registry, Environment environment, + List runningServices) { for (RunningService runningService : runningServices) { - DockerComposeConnectionSource source = new DockerComposeConnectionSource(runningService); + DockerComposeConnectionSource source = new DockerComposeConnectionSource(runningService, environment); this.factories.getConnectionDetails(source, false).forEach((connectionDetailsType, connectionDetails) -> { register(registry, runningService, connectionDetailsType, connectionDetails); this.factories.getConnectionDetails(connectionDetails, false) diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactory.java index 3f4ad8653d26..097283371cd8 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactory.java @@ -16,11 +16,16 @@ package org.springframework.boot.docker.compose.service.connection.postgres; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; import org.springframework.boot.docker.compose.core.RunningService; import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; /** * {@link DockerComposeConnectionDetailsFactory} to create {@link JdbcConnectionDetails} @@ -42,7 +47,7 @@ protected PostgresJdbcDockerComposeConnectionDetailsFactory() { @Override protected JdbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { - return new PostgresJdbcDockerComposeConnectionDetails(source.getRunningService()); + return new PostgresJdbcDockerComposeConnectionDetails(source.getRunningService(), source.getEnvironment()); } /** @@ -57,10 +62,11 @@ static class PostgresJdbcDockerComposeConnectionDetails extends DockerComposeCon private final String jdbcUrl; - PostgresJdbcDockerComposeConnectionDetails(RunningService service) { + PostgresJdbcDockerComposeConnectionDetails(RunningService service, Environment environment) { super(service); this.environment = new PostgresEnvironment(service.env()); - this.jdbcUrl = jdbcUrlBuilder.build(service, this.environment.getDatabase()); + this.jdbcUrl = addApplicationNameIfNecessary(jdbcUrlBuilder.build(service, this.environment.getDatabase()), + environment); } @Override @@ -78,6 +84,27 @@ public String getJdbcUrl() { return this.jdbcUrl; } + private static String addApplicationNameIfNecessary(String jdbcUrl, Environment environment) { + if (jdbcUrl.contains("&ApplicationName=") || jdbcUrl.contains("?ApplicationName=")) { + return jdbcUrl; + } + String applicationName = environment.getProperty("spring.application.name"); + if (!StringUtils.hasText(applicationName)) { + return jdbcUrl; + } + StringBuilder jdbcUrlBuilder = new StringBuilder(jdbcUrl); + if (!jdbcUrl.contains("?")) { + jdbcUrlBuilder.append("?"); + } + else if (!jdbcUrl.endsWith("&")) { + jdbcUrlBuilder.append("&"); + } + return jdbcUrlBuilder.append("ApplicationName") + .append('=') + .append(URLEncoder.encode(applicationName, StandardCharsets.UTF_8)) + .toString(); + } + } } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactory.java index cb1c66ded50b..6da2bbcd23c5 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactory.java @@ -17,12 +17,15 @@ package org.springframework.boot.docker.compose.service.connection.postgres; import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.Option; import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails; import org.springframework.boot.docker.compose.core.RunningService; import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; import org.springframework.boot.docker.compose.service.connection.r2dbc.ConnectionFactoryOptionsBuilder; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; /** * {@link DockerComposeConnectionDetailsFactory} to create {@link R2dbcConnectionDetails} @@ -44,7 +47,7 @@ class PostgresR2dbcDockerComposeConnectionDetailsFactory @Override protected R2dbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { - return new PostgresDbR2dbcDockerComposeConnectionDetails(source.getRunningService()); + return new PostgresDbR2dbcDockerComposeConnectionDetails(source.getRunningService(), source.getEnvironment()); } /** @@ -53,16 +56,16 @@ protected R2dbcConnectionDetails getDockerComposeConnectionDetails(DockerCompose static class PostgresDbR2dbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails implements R2dbcConnectionDetails { + private static final Option APPLICATION_NAME = Option.valueOf("applicationName"); + private static final ConnectionFactoryOptionsBuilder connectionFactoryOptionsBuilder = new ConnectionFactoryOptionsBuilder( "postgresql", 5432); private final ConnectionFactoryOptions connectionFactoryOptions; - PostgresDbR2dbcDockerComposeConnectionDetails(RunningService service) { + PostgresDbR2dbcDockerComposeConnectionDetails(RunningService service, Environment environment) { super(service); - PostgresEnvironment environment = new PostgresEnvironment(service.env()); - this.connectionFactoryOptions = connectionFactoryOptionsBuilder.build(service, environment.getDatabase(), - environment.getUsername(), environment.getPassword()); + this.connectionFactoryOptions = getConnectionFactoryOptions(service, environment); } @Override @@ -70,6 +73,26 @@ public ConnectionFactoryOptions getConnectionFactoryOptions() { return this.connectionFactoryOptions; } + private static ConnectionFactoryOptions getConnectionFactoryOptions(RunningService service, + Environment environment) { + PostgresEnvironment env = new PostgresEnvironment(service.env()); + ConnectionFactoryOptions connectionFactoryOptions = connectionFactoryOptionsBuilder.build(service, + env.getDatabase(), env.getUsername(), env.getPassword()); + return addApplicationNameIfNecessary(connectionFactoryOptions, environment); + } + + private static ConnectionFactoryOptions addApplicationNameIfNecessary( + ConnectionFactoryOptions connectionFactoryOptions, Environment environment) { + if (connectionFactoryOptions.hasOption(APPLICATION_NAME)) { + return connectionFactoryOptions; + } + String applicationName = environment.getProperty("spring.application.name"); + if (!StringUtils.hasText(applicationName)) { + return connectionFactoryOptions; + } + return connectionFactoryOptions.mutate().option(APPLICATION_NAME, applicationName).build(); + } + } } diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactoryConnectionDetailsTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactoryConnectionDetailsTests.java new file mode 100644 index 000000000000..535184f167e5 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactoryConnectionDetailsTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2012-2024 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.boot.docker.compose.service.connection.postgres; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; +import org.springframework.boot.docker.compose.core.ConnectionPorts; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for + * {@link PostgresJdbcDockerComposeConnectionDetailsFactory.PostgresJdbcDockerComposeConnectionDetails}. + * + * @author Dmytro Nosan + */ +class PostgresJdbcDockerComposeConnectionDetailsFactoryConnectionDetailsTests { + + private final RunningService service = mock(RunningService.class); + + private final MockEnvironment environment = new MockEnvironment(); + + private final Map labels = new LinkedHashMap<>(); + + PostgresJdbcDockerComposeConnectionDetailsFactoryConnectionDetailsTests() { + given(this.service.env()) + .willReturn(Map.of("POSTGRES_USER", "user", "POSTGRES_PASSWORD", "password", "POSTGRES_DB", "database")); + given(this.service.labels()).willReturn(this.labels); + ConnectionPorts connectionPorts = mock(ConnectionPorts.class); + given(this.service.ports()).willReturn(connectionPorts); + given(this.service.host()).willReturn("localhost"); + given(connectionPorts.get(5432)).willReturn(30001); + } + + @Test + void createConnectionDetails() { + JdbcConnectionDetails connectionDetails = getConnectionDetails(); + assertConnectionDetails(connectionDetails); + assertThat(connectionDetails.getJdbcUrl()).endsWith("/database"); + } + + @Test + void createConnectionDetailsWithLabels() { + this.labels.put("org.springframework.boot.jdbc.parameters", "connectTimeout=30&ApplicationName=spring-boot"); + JdbcConnectionDetails connectionDetails = getConnectionDetails(); + assertConnectionDetails(connectionDetails); + assertThat(connectionDetails.getJdbcUrl()).endsWith("?connectTimeout=30&ApplicationName=spring-boot"); + } + + @Test + void createConnectionDetailsWithApplicationNameLabelTakesPrecedence() { + this.labels.put("org.springframework.boot.jdbc.parameters", "ApplicationName=spring-boot"); + this.environment.setProperty("spring.application.name", "my-app"); + JdbcConnectionDetails connectionDetails = getConnectionDetails(); + assertConnectionDetails(connectionDetails); + assertThat(connectionDetails.getJdbcUrl()).endsWith("?ApplicationName=spring-boot"); + } + + @Test + void createConnectionDetailsWithSpringApplicationName() { + this.environment.setProperty("spring.application.name", "spring boot"); + JdbcConnectionDetails connectionDetails = getConnectionDetails(); + assertConnectionDetails(connectionDetails); + assertThat(connectionDetails.getJdbcUrl()).endsWith("?ApplicationName=spring+boot"); + } + + @Test + void createConnectionDetailsAppendSpringApplicationName() { + this.labels.put("org.springframework.boot.jdbc.parameters", "connectTimeout=30"); + this.environment.setProperty("spring.application.name", "spring boot"); + JdbcConnectionDetails connectionDetails = getConnectionDetails(); + assertConnectionDetails(connectionDetails); + assertThat(connectionDetails.getJdbcUrl()).endsWith("?connectTimeout=30&ApplicationName=spring+boot"); + } + + @Test + void createConnectionDetailsAppendSpringApplicationNameParametersEndedWithAmpersand() { + this.labels.put("org.springframework.boot.jdbc.parameters", "connectTimeout=30&"); + this.environment.setProperty("spring.application.name", "spring boot"); + JdbcConnectionDetails connectionDetails = getConnectionDetails(); + assertConnectionDetails(connectionDetails); + assertThat(connectionDetails.getJdbcUrl()).endsWith("?connectTimeout=30&ApplicationName=spring+boot"); + + } + + private void assertConnectionDetails(JdbcConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUsername()).isEqualTo("user"); + assertThat(connectionDetails.getPassword()).isEqualTo("password"); + assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:postgresql://localhost:30001/database"); + assertThat(connectionDetails.getDriverClassName()).isEqualTo("org.postgresql.Driver"); + } + + private JdbcConnectionDetails getConnectionDetails() { + return new PostgresJdbcDockerComposeConnectionDetailsFactory.PostgresJdbcDockerComposeConnectionDetails( + this.service, this.environment); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactoryConnectionDetailsTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactoryConnectionDetailsTests.java new file mode 100644 index 000000000000..26f78283e952 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactoryConnectionDetailsTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2012-2024 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.boot.docker.compose.service.connection.postgres; + +import java.util.LinkedHashMap; +import java.util.Map; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.Option; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.docker.compose.core.ConnectionPorts; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for + * {@link PostgresR2dbcDockerComposeConnectionDetailsFactory.PostgresDbR2dbcDockerComposeConnectionDetails}. + * + * @author Dmytro Nosan + */ +class PostgresR2dbcDockerComposeConnectionDetailsFactoryConnectionDetailsTests { + + private static final Option APPLICATION_NAME = Option.valueOf("applicationName"); + + private final RunningService service = mock(RunningService.class); + + private final MockEnvironment environment = new MockEnvironment(); + + private final Map labels = new LinkedHashMap<>(); + + PostgresR2dbcDockerComposeConnectionDetailsFactoryConnectionDetailsTests() { + given(this.service.env()) + .willReturn(Map.of("POSTGRES_USER", "myuser", "POSTGRES_PASSWORD", "secret", "POSTGRES_DB", "mydatabase")); + given(this.service.labels()).willReturn(this.labels); + ConnectionPorts connectionPorts = mock(ConnectionPorts.class); + given(this.service.ports()).willReturn(connectionPorts); + given(this.service.host()).willReturn("localhost"); + given(connectionPorts.get(5432)).willReturn(30001); + } + + @Test + void createConnectionDetails() { + ConnectionFactoryOptions options = getConnectionFactoryOptions(); + assertConnectionFactoryOptions(options); + assertThat(options.getValue(APPLICATION_NAME)).isNull(); + } + + @Test + void createConnectionDetailsWithLabels() { + this.labels.put("org.springframework.boot.r2dbc.parameters", + "connectTimeout=PT15S,applicationName=spring-boot"); + ConnectionFactoryOptions options = getConnectionFactoryOptions(); + assertConnectionFactoryOptions(options); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.CONNECT_TIMEOUT)).isEqualTo("PT15S"); + assertThat(options.getRequiredValue(APPLICATION_NAME)).isEqualTo("spring-boot"); + } + + @Test + void createConnectionDetailsWithApplicationNameLabelTakesPrecedence() { + this.labels.put("org.springframework.boot.r2dbc.parameters", "applicationName=spring-boot"); + this.environment.setProperty("spring.application.name", "my-app"); + ConnectionFactoryOptions options = getConnectionFactoryOptions(); + assertConnectionFactoryOptions(options); + assertThat(options.getRequiredValue(APPLICATION_NAME)).isEqualTo("spring-boot"); + } + + @Test + void createConnectionDetailsWithSpringApplicationName() { + this.environment.setProperty("spring.application.name", "spring boot"); + ConnectionFactoryOptions options = getConnectionFactoryOptions(); + assertConnectionFactoryOptions(options); + assertThat(options.getRequiredValue(APPLICATION_NAME)).isEqualTo("spring boot"); + } + + @Test + void createConnectionDetailsAppendSpringApplicationName() { + this.labels.put("org.springframework.boot.r2dbc.parameters", "connectTimeout=PT15S"); + this.environment.setProperty("spring.application.name", "my-app"); + ConnectionFactoryOptions options = getConnectionFactoryOptions(); + assertConnectionFactoryOptions(options); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.CONNECT_TIMEOUT)).isEqualTo("PT15S"); + assertThat(options.getRequiredValue(APPLICATION_NAME)).isEqualTo("my-app"); + } + + private void assertConnectionFactoryOptions(ConnectionFactoryOptions options) { + assertThat(options.getRequiredValue(ConnectionFactoryOptions.HOST)).isEqualTo("localhost"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.PORT)).isEqualTo(30001); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("mydatabase"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.USER)).isEqualTo("myuser"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("secret"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("postgresql"); + } + + private ConnectionFactoryOptions getConnectionFactoryOptions() { + return new PostgresR2dbcDockerComposeConnectionDetailsFactory.PostgresDbR2dbcDockerComposeConnectionDetails( + this.service, this.environment) + .getConnectionFactoryOptions(); + } + +}