From 609d1fceb877140082c5559cb829761642a37c40 Mon Sep 17 00:00:00 2001 From: Cedric Champeau Date: Thu, 23 Nov 2023 10:28:47 +0100 Subject: [PATCH] Propagate more errors to the client side This commit improves error reporting on the client side. It propagates some errors which were previously only captured on the server side. In particular, it will now: - report which property was being resolved, instead of showing internal `auto.test.resources` properties - capture errors which are sent by the server but were dropped because of lack of JSON error handling - report when a container cannot be started or failed during startup - provide clearer message when the test resources service is down Fixes #444 --- gradle/libs.versions.toml | 2 +- test-resources-client/build.gradle | 6 +- .../client/DefaultTestResourcesClient.java | 55 ++++++++++++++++- .../client/SimpleJsonErrorModel.java | 33 ++++++++++ ...urcesClientPropertyExpressionResolver.java | 22 ++++++- .../client/TestResourcesException.java | 4 ++ .../NoServerTestResourcesClientTest.groovy | 35 +++++++++++ .../TestResourcesClientPropertiesTest.groovy | 6 +- .../client/TestResourcesClientTest.groovy | 18 +++++- .../testresources/client/TestServer.groovy | 5 +- .../LazyTestResourcesExpressionResolver.java | 10 ++- .../TestResourcesResolutionException.java | 34 +++++++++++ .../jdbc/xe/OracleATPTest.groovy | 2 +- .../r2dbc/oracle/OracleATPReactiveTest.groovy | 8 +-- .../AbstractTestContainersProvider.java | 8 --- .../testcontainers/TestContainers.java | 61 +++++++++++-------- .../testcontainers/IncorrectImageTest.groovy | 22 +++++++ .../src/test/resources/application-test.yml | 3 + 18 files changed, 279 insertions(+), 55 deletions(-) create mode 100644 test-resources-client/src/main/java/io/micronaut/testresources/client/SimpleJsonErrorModel.java create mode 100644 test-resources-client/src/test/groovy/io/micronaut/testresources/client/NoServerTestResourcesClientTest.groovy create mode 100644 test-resources-core/src/main/java/io/micronaut/testresources/core/TestResourcesResolutionException.java create mode 100644 test-resources-testcontainers/src/test/groovy/io/micronaut/testresources/testcontainers/IncorrectImageTest.groovy diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f50a5057e..996e82447 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,7 +37,7 @@ micronaut-reactor = "3.1.0" micronaut-gradle-plugin = "4.1.2" micronaut-redis = "6.0.2" micronaut-security = "4.1.0" -micronaut-serde = "2.2.6" +micronaut-serde = "2.4.0" micronaut-sql = "5.2.0" micronaut-test = "4.0.0" groovy = "4.0.13" diff --git a/test-resources-client/build.gradle b/test-resources-client/build.gradle index d3dc4b7e8..9d871d546 100644 --- a/test-resources-client/build.gradle +++ b/test-resources-client/build.gradle @@ -9,8 +9,10 @@ and provide their value on demand. """ dependencies { + annotationProcessor(mnSerde.micronaut.serde.processor) api(mn.micronaut.json.core) - api(project(':micronaut-test-resources-core')) - + api(projects.micronautTestResourcesCore) + compileOnly(mnSerde.micronaut.serde.api) + compileOnly(mnSerde.micronaut.serde.jackson) testRuntimeOnly(mn.micronaut.http.server.netty) } diff --git a/test-resources-client/src/main/java/io/micronaut/testresources/client/DefaultTestResourcesClient.java b/test-resources-client/src/main/java/io/micronaut/testresources/client/DefaultTestResourcesClient.java index d6f20dc12..f7d280829 100644 --- a/test-resources-client/src/main/java/io/micronaut/testresources/client/DefaultTestResourcesClient.java +++ b/test-resources-client/src/main/java/io/micronaut/testresources/client/DefaultTestResourcesClient.java @@ -21,6 +21,7 @@ import io.micronaut.json.JsonMapper; import java.io.IOException; +import java.net.ConnectException; import java.net.URI; import java.net.URISyntaxException; import java.net.http.HttpClient; @@ -29,6 +30,7 @@ import java.time.Duration; import java.util.Collection; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -51,6 +53,8 @@ public class DefaultTestResourcesClient implements TestResourcesClient { private static final Argument> LIST_OF_STRING = Argument.LIST_OF_STRING; private static final Argument STRING = Argument.STRING; private static final Argument BOOLEAN = Argument.BOOLEAN; + private static final String INTERNAL_SERVER_ERROR_PREFIX = "Internal Server Error: "; + private static final String INTERNAL_SERVER_ERROR = "Internal Server Error"; private final JsonMapper jsonMapper; private final String baseUri; @@ -81,7 +85,8 @@ public List getResolvableProperties(Map> prop } @Override - public Optional resolve(String name, Map properties, Map testResourcesConfig) { + public Optional resolve(String name, Map properties, + Map testResourcesConfig) { Map params = new HashMap<>(); params.put("name", name); params.put("properties", properties); @@ -119,7 +124,8 @@ private void GET(HttpRequest.Builder request) { request.GET(); } - private T request(String path, Argument type, Consumer config) { + private T request(String path, Argument type, + Consumer config) { var request = HttpRequest.newBuilder() .uri(uri(path)) .timeout(clientTimeout); @@ -138,8 +144,15 @@ private T request(String path, Argument type, Consumer T request(String path, Argument type, Consumer T handleError(SimpleJsonErrorModel model) { + var allErrors = new LinkedHashSet(); + collectErrors(model, allErrors); + var errorList = allErrors.stream().toList(); + if (errorList.size() == 1) { + throw new TestResourcesException(errorList.get(0)); + } else { + var sb = new StringBuilder(); + sb.append("Server failed with the following errors:\n"); + for (String error : errorList) { + sb.append(" - ").append(error).append("\n"); + } + throw new TestResourcesException(sb.toString()); + } + } + + private void collectErrors(SimpleJsonErrorModel model, LinkedHashSet allErrors) { + sanitizeError(model.message()).ifPresent(allErrors::add); + if (model.embedded() != null && model.embedded().errors() != null) { + for (SimpleJsonErrorModel error : model.embedded().errors()) { + collectErrors(error, allErrors); + } + } + } + + private static Optional sanitizeError(String message) { + if (message.equals(INTERNAL_SERVER_ERROR)) { + return Optional.empty(); + } + if (message.startsWith(INTERNAL_SERVER_ERROR_PREFIX)) { + return Optional.of(message.substring(INTERNAL_SERVER_ERROR_PREFIX.length())); + } + return Optional.of(message); + } + private URI uri(String path) { try { return new URI(baseUri + path); @@ -163,4 +211,5 @@ private byte[] writeValueAsBytes(Object o) { throw new TestResourcesException(e); } } + } diff --git a/test-resources-client/src/main/java/io/micronaut/testresources/client/SimpleJsonErrorModel.java b/test-resources-client/src/main/java/io/micronaut/testresources/client/SimpleJsonErrorModel.java new file mode 100644 index 000000000..d99dee6ad --- /dev/null +++ b/test-resources-client/src/main/java/io/micronaut/testresources/client/SimpleJsonErrorModel.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2023 original 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 io.micronaut.testresources.client; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.micronaut.serde.annotation.Serdeable; + +import java.util.List; + +@Serdeable +public record SimpleJsonErrorModel( + @JsonProperty("message") String message, + @JsonProperty("_embedded") Embedded embedded +) { + @Serdeable + public record Embedded( + @JsonProperty("errors") List errors + ) { + } +} diff --git a/test-resources-client/src/main/java/io/micronaut/testresources/client/TestResourcesClientPropertyExpressionResolver.java b/test-resources-client/src/main/java/io/micronaut/testresources/client/TestResourcesClientPropertyExpressionResolver.java index 1ebc12bd4..c8bea971e 100644 --- a/test-resources-client/src/main/java/io/micronaut/testresources/client/TestResourcesClientPropertyExpressionResolver.java +++ b/test-resources-client/src/main/java/io/micronaut/testresources/client/TestResourcesClientPropertyExpressionResolver.java @@ -31,6 +31,7 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; import static io.micronaut.testresources.core.PropertyResolverSupport.resolveRequiredProperties; @@ -108,7 +109,26 @@ public Optional resolve(PropertyResolver propertyResolver, } private static Optional callClient(String expression, TestResourcesClient client, Map props, Map properties) { - return client.resolve(expression, props, properties); + return withErrorHandling( + () -> client.resolve(expression, props, properties), + () -> "Test resources service wasn't able to revolve expression '" + expression + "'" + ); + } + + private static T withErrorHandling(Supplier callable, Supplier errorMessage) { + try { + return callable.get(); + } catch (TestResourcesException ex) { + var sb = new StringBuilder(); + sb.append(errorMessage.get()).append(":"); + var message = ex.getMessage(); + if (message.contains("\n")) { + sb.append(" ").append(message); + } else { + sb.append(" ").append(message); + } + throw new TestResourcesException(sb.toString()); + } } @Override diff --git a/test-resources-client/src/main/java/io/micronaut/testresources/client/TestResourcesException.java b/test-resources-client/src/main/java/io/micronaut/testresources/client/TestResourcesException.java index 4bfba2786..db69b0d09 100644 --- a/test-resources-client/src/main/java/io/micronaut/testresources/client/TestResourcesException.java +++ b/test-resources-client/src/main/java/io/micronaut/testresources/client/TestResourcesException.java @@ -23,6 +23,10 @@ public TestResourcesException(Throwable cause) { super(cause); } + public TestResourcesException(String message, Throwable cause) { + super(message, cause); + } + public TestResourcesException(String message) { super(message); } diff --git a/test-resources-client/src/test/groovy/io/micronaut/testresources/client/NoServerTestResourcesClientTest.groovy b/test-resources-client/src/test/groovy/io/micronaut/testresources/client/NoServerTestResourcesClientTest.groovy new file mode 100644 index 000000000..6bdf83721 --- /dev/null +++ b/test-resources-client/src/test/groovy/io/micronaut/testresources/client/NoServerTestResourcesClientTest.groovy @@ -0,0 +1,35 @@ +package io.micronaut.testresources.client + +import io.micronaut.context.ApplicationContext +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import spock.lang.Specification +import spock.util.environment.RestoreSystemProperties + +import static io.micronaut.testresources.client.ConfigFinder.systemPropertyNameOf + +@MicronautTest +class NoServerTestResourcesClientTest extends Specification implements ClientCleanup { + + private static final String DUMMY_URL = "https://localhost:666/nope" + + @RestoreSystemProperties + def "reasonable error message if the server isn't running"() { + + when: + createApplication() + + then: + TestResourcesException e = thrown() + e.message == "Test resource service is not available at $DUMMY_URL" + } + + private ApplicationContext createApplication() { + System.setProperty(systemPropertyNameOf(TestResourcesClient.SERVER_URI), DUMMY_URL) + def app = ApplicationContext.builder() + .properties(['server': 'false']) + .start() + assert !app.findBean(TestServer).present + return app + } + +} diff --git a/test-resources-client/src/test/groovy/io/micronaut/testresources/client/TestResourcesClientPropertiesTest.groovy b/test-resources-client/src/test/groovy/io/micronaut/testresources/client/TestResourcesClientPropertiesTest.groovy index 3c5dbad4d..fb056e650 100644 --- a/test-resources-client/src/test/groovy/io/micronaut/testresources/client/TestResourcesClientPropertiesTest.groovy +++ b/test-resources-client/src/test/groovy/io/micronaut/testresources/client/TestResourcesClientPropertiesTest.groovy @@ -1,9 +1,9 @@ package io.micronaut.testresources.client import io.micronaut.context.ApplicationContext -import io.micronaut.context.exceptions.ConfigurationException import io.micronaut.runtime.server.EmbeddedServer import io.micronaut.test.extensions.spock.annotation.MicronautTest +import io.micronaut.testresources.core.TestResourcesResolutionException import jakarta.inject.Inject import spock.lang.Specification import spock.util.environment.RestoreSystemProperties @@ -29,8 +29,8 @@ class TestResourcesClientPropertiesTest extends Specification implements ClientC app.getProperty("missing", String).empty then: - ConfigurationException e = thrown() - e.message == 'Could not resolve placeholder ${auto.test.resources.missing}' + TestResourcesResolutionException e = thrown() + e.message == "Test resources doesn't support resolving expression 'missing'" cleanup: System.clearProperty("micronaut.test.resources.server.uri") diff --git a/test-resources-client/src/test/groovy/io/micronaut/testresources/client/TestResourcesClientTest.groovy b/test-resources-client/src/test/groovy/io/micronaut/testresources/client/TestResourcesClientTest.groovy index ebdc845c3..9b77da5b3 100644 --- a/test-resources-client/src/test/groovy/io/micronaut/testresources/client/TestResourcesClientTest.groovy +++ b/test-resources-client/src/test/groovy/io/micronaut/testresources/client/TestResourcesClientTest.groovy @@ -1,9 +1,9 @@ package io.micronaut.testresources.client import io.micronaut.context.ApplicationContext -import io.micronaut.context.exceptions.ConfigurationException import io.micronaut.runtime.server.EmbeddedServer import io.micronaut.test.extensions.spock.annotation.MicronautTest +import io.micronaut.testresources.core.TestResourcesResolutionException import jakarta.inject.Inject import spock.lang.Specification import spock.lang.TempDir @@ -34,8 +34,20 @@ class TestResourcesClientTest extends Specification implements ClientCleanup { app.getProperty("missing", String).empty then: - ConfigurationException e = thrown() - e.message == 'Could not resolve placeholder ${auto.test.resources.missing}' + TestResourcesResolutionException e = thrown() + e.message == "Test resources doesn't support resolving expression 'missing'" + } + + @RestoreSystemProperties + def "reasonable error message when the server throws an error"() { + def app = createApplication() + + when: + app.getProperty("throws", String) + + then: + TestResourcesException e = thrown() + e.message == "Test resources service wasn't able to revolve expression 'throws': Something bad happened" } private ApplicationContext createApplication() { diff --git a/test-resources-client/src/test/groovy/io/micronaut/testresources/client/TestServer.groovy b/test-resources-client/src/test/groovy/io/micronaut/testresources/client/TestServer.groovy index 14c04d8e6..4c280291e 100644 --- a/test-resources-client/src/test/groovy/io/micronaut/testresources/client/TestServer.groovy +++ b/test-resources-client/src/test/groovy/io/micronaut/testresources/client/TestServer.groovy @@ -13,7 +13,7 @@ class TestServer implements TestResourcesResolver { @Override @Post("/list") List getResolvableProperties(Map> propertyEntries, Map testResourcesConfig) { - ["dummy1", "dummy2", "missing"] + ["dummy1", "dummy2", "missing", "throws"] } @Override @@ -34,6 +34,9 @@ class TestServer implements TestResourcesResolver { if ("missing" == name) { return Optional.empty() } + if ("throws" == name) { + throw new RuntimeException("Something bad happened") + } Optional.of("value for $name".toString()) } diff --git a/test-resources-core/src/main/java/io/micronaut/testresources/core/LazyTestResourcesExpressionResolver.java b/test-resources-core/src/main/java/io/micronaut/testresources/core/LazyTestResourcesExpressionResolver.java index cf9f9998b..21194d889 100644 --- a/test-resources-core/src/main/java/io/micronaut/testresources/core/LazyTestResourcesExpressionResolver.java +++ b/test-resources-core/src/main/java/io/micronaut/testresources/core/LazyTestResourcesExpressionResolver.java @@ -42,7 +42,15 @@ public Optional resolve(PropertyResolver propertyResolver, Class requiredType) { if (expression.startsWith(PLACEHOLDER_PREFIX)) { String eagerExpression = expression.substring(PLACEHOLDER_PREFIX.length()); - return delegate.resolve(propertyResolver, conversionService, eagerExpression, requiredType); + var resolved = delegate.resolve(propertyResolver, conversionService, eagerExpression, + requiredType); + if (resolved.isEmpty()) { + // This is the only case where we should throw an exception instead of returning + // an empty optional: we assume that if the expression starts with the prefix + // then ONLY test resources should resolve it + throw new TestResourcesResolutionException("Test resources doesn't support resolving expression '" + expression.substring(PLACEHOLDER_PREFIX.length()) + "'"); + } + return resolved; } return Optional.empty(); } diff --git a/test-resources-core/src/main/java/io/micronaut/testresources/core/TestResourcesResolutionException.java b/test-resources-core/src/main/java/io/micronaut/testresources/core/TestResourcesResolutionException.java new file mode 100644 index 000000000..aa6452c43 --- /dev/null +++ b/test-resources-core/src/main/java/io/micronaut/testresources/core/TestResourcesResolutionException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2021 original 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 io.micronaut.testresources.core; + +/** + * An exception thrown whenever test resources was expected to resolve + * a property but couldn't. + */ +public class TestResourcesResolutionException extends RuntimeException { + public TestResourcesResolutionException(Throwable cause) { + super(cause); + } + + public TestResourcesResolutionException(String message, Throwable cause) { + super(message, cause); + } + + public TestResourcesResolutionException(String message) { + super(message); + } +} diff --git a/test-resources-jdbc/test-resources-jdbc-oracle-xe/src/test/groovy/io/micronaut/testresources/jdbc/xe/OracleATPTest.groovy b/test-resources-jdbc/test-resources-jdbc-oracle-xe/src/test/groovy/io/micronaut/testresources/jdbc/xe/OracleATPTest.groovy index 4a4f20863..846563f56 100644 --- a/test-resources-jdbc/test-resources-jdbc-oracle-xe/src/test/groovy/io/micronaut/testresources/jdbc/xe/OracleATPTest.groovy +++ b/test-resources-jdbc/test-resources-jdbc-oracle-xe/src/test/groovy/io/micronaut/testresources/jdbc/xe/OracleATPTest.groovy @@ -19,7 +19,7 @@ class OracleATPTest extends AbstractJDBCSpec { then: BeanInstantiationException ex = thrown() - ex.message.contains("Could not resolve placeholder \${auto.test.resources.datasources.default") + ex.message.contains("Test resources doesn't support resolving expression 'datasources.default.password'") } @Override diff --git a/test-resources-r2dbc/test-resources-r2dbc-oracle-xe/src/test/groovy/io/micronaut/testresources/r2dbc/oracle/OracleATPReactiveTest.groovy b/test-resources-r2dbc/test-resources-r2dbc-oracle-xe/src/test/groovy/io/micronaut/testresources/r2dbc/oracle/OracleATPReactiveTest.groovy index 8f71f10bf..1747bd15e 100644 --- a/test-resources-r2dbc/test-resources-r2dbc-oracle-xe/src/test/groovy/io/micronaut/testresources/r2dbc/oracle/OracleATPReactiveTest.groovy +++ b/test-resources-r2dbc/test-resources-r2dbc-oracle-xe/src/test/groovy/io/micronaut/testresources/r2dbc/oracle/OracleATPReactiveTest.groovy @@ -19,12 +19,12 @@ class OracleATPReactiveTest extends AbstractJDBCSpec { then: BeanInstantiationException ex = thrown() - ex.message.contains("Could not resolve placeholder \${$placeholder") + ex.message.contains("Test resources doesn't support resolving expression '$expression") where: - environments | placeholder - ["test", "jdbc", "prod"] | "auto.test.resources.datasources.default" - ["test", "standalone", "standaloneprod"] | "auto.test.resources.r2dbc.datasources.default" + environments | expression + ["test", "jdbc", "prod"] | "datasources.default" + ["test", "standalone", "standaloneprod"] | "r2dbc.datasources.default" } diff --git a/test-resources-testcontainers/src/main/java/io/micronaut/testresources/testcontainers/AbstractTestContainersProvider.java b/test-resources-testcontainers/src/main/java/io/micronaut/testresources/testcontainers/AbstractTestContainersProvider.java index 3a8bc2f24..e35de1c0a 100644 --- a/test-resources-testcontainers/src/main/java/io/micronaut/testresources/testcontainers/AbstractTestContainersProvider.java +++ b/test-resources-testcontainers/src/main/java/io/micronaut/testresources/testcontainers/AbstractTestContainersProvider.java @@ -42,14 +42,6 @@ public int getOrder() { return SPECIFIC_ORDER; } - @Override - public boolean isEnabled(Map testResourcesConfig) { - if (!DockerSupport.isDockerAvailable()) { - return false; - } - return ToggableTestResourcesResolver.super.isEnabled(testResourcesConfig); - } - /** * Returns the name of the resource resolver, for example "kafka" or "mysql". * diff --git a/test-resources-testcontainers/src/main/java/io/micronaut/testresources/testcontainers/TestContainers.java b/test-resources-testcontainers/src/main/java/io/micronaut/testresources/testcontainers/TestContainers.java index b6d12f9a3..2d82a1a0a 100644 --- a/test-resources-testcontainers/src/main/java/io/micronaut/testresources/testcontainers/TestContainers.java +++ b/test-resources-testcontainers/src/main/java/io/micronaut/testresources/testcontainers/TestContainers.java @@ -16,8 +16,10 @@ package io.micronaut.testresources.testcontainers; import io.micronaut.testresources.core.Scope; +import io.micronaut.testresources.core.TestResourcesResolutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.testcontainers.containers.ContainerFetchException; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.utility.DockerImageName; @@ -108,37 +110,42 @@ static > T getOrCreate(String requestedP Supplier imageNameSupplier, Function creator) { return withKey(Key.of(owner, name, Scope.from(query), query), key -> { - T container = withMapLock("getOrCreate", () -> (T) CONTAINERS_BY_KEY.get(key)); - var dockerImageName = imageNameSupplier.get(); - if (container == null) { - notifyStartOperation(PULLING, dockerImageName); - try { - container = creator.apply(dockerImageName); - } finally { - notifyEndOperation(PULLING, dockerImageName); - } - try { - notifyStartOperation(STARTING, dockerImageName); - if (DockerSupport.isDockerAvailable()) { - LOGGER.info("Starting test container {}", name); - container.start(); - } else { - LOGGER.error("Cannot start container {} as Docker support isn't available", - name); + try { + T container = withMapLock("getOrCreate", () -> (T) CONTAINERS_BY_KEY.get(key)); + var dockerImageName = imageNameSupplier.get(); + if (container == null) { + notifyStartOperation(PULLING, dockerImageName); + try { + container = creator.apply(dockerImageName); + } finally { + notifyEndOperation(PULLING, dockerImageName); + } + try { + notifyStartOperation(STARTING, dockerImageName); + if (DockerSupport.isDockerAvailable()) { + LOGGER.info("Starting test container {}", name); + container.start(); + } else { + throw new TestResourcesResolutionException("Cannot start container " + name + " as Docker doesn't seem to be available"); + } + } finally { + notifyEndOperation(STARTING, dockerImageName); } - } finally { - notifyEndOperation(STARTING, dockerImageName); + T finalContainer = container; + withMapLock("getOrCreate", () -> CONTAINERS_BY_KEY.put(key, finalContainer)); } T finalContainer = container; - withMapLock("getOrCreate", () -> CONTAINERS_BY_KEY.put(key, finalContainer)); + withMapLock("getOrCreate", () -> + CONTAINERS_BY_PROPERTY.computeIfAbsent(requestedProperty, + e -> new LinkedHashSet<>()) + .add(finalContainer) + ); + return container; + } catch (ContainerFetchException ex) { + // unwrap message for clearer error on the client side + var message = ex.getCause().getMessage(); + throw new TestResourcesResolutionException(message); } - T finalContainer = container; - withMapLock("getOrCreate", () -> - CONTAINERS_BY_PROPERTY.computeIfAbsent(requestedProperty, - e -> new LinkedHashSet<>()) - .add(finalContainer) - ); - return container; }); } diff --git a/test-resources-testcontainers/src/test/groovy/io/micronaut/testresources/testcontainers/IncorrectImageTest.groovy b/test-resources-testcontainers/src/test/groovy/io/micronaut/testresources/testcontainers/IncorrectImageTest.groovy new file mode 100644 index 000000000..3a2cf0b48 --- /dev/null +++ b/test-resources-testcontainers/src/test/groovy/io/micronaut/testresources/testcontainers/IncorrectImageTest.groovy @@ -0,0 +1,22 @@ +package io.micronaut.testresources.testcontainers + +import io.micronaut.context.ApplicationContext +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Specification + +@MicronautTest +class IncorrectImageTest extends Specification { + + @Inject + ApplicationContext applicationContext + + void "reasonable error message if an image doesn't exist"() { + when: + applicationContext.getProperty("incorrect-image.host", String) + + then: + Exception ex = thrown() + ex.message.contains """Status 404: {"message":"pull access denied for this-image-does-not-exist, repository does not exist or may require 'docker login': denied: requested access to the resource is denied"}""" + } +} diff --git a/test-resources-testcontainers/src/test/resources/application-test.yml b/test-resources-testcontainers/src/test/resources/application-test.yml index 4ab13f948..1bd65d210 100644 --- a/test-resources-testcontainers/src/test/resources/application-test.yml +++ b/test-resources-testcontainers/src/test/resources/application-test.yml @@ -22,3 +22,6 @@ test-resources: network-aliases: alice hostnames: consumer.host depends-on: producer + incorrect-image: + image-name: this-image-does-not-exist:1.0 + hostnames: incorrect-image.host