This is an automated email from the ASF dual-hosted git repository. jamesnetherton pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
commit 796fe87c1d84880c57651c50176a40ca79191a5d Author: James Netherton <[email protected]> AuthorDate: Wed Oct 28 08:52:58 2020 +0000 Add WireMock test support --- integration-tests-support/pom.xml | 1 + integration-tests-support/wiremock/README.adoc | 48 +++++ integration-tests-support/wiremock/pom.xml | 62 ++++++ .../camel/quarkus/test/wiremock/MockServer.java | 32 ++++ .../WireMockTestResourceLifecycleManager.java | 211 +++++++++++++++++++++ pom.xml | 1 + poms/bom-test/pom.xml | 28 +++ 7 files changed, 383 insertions(+) diff --git a/integration-tests-support/pom.xml b/integration-tests-support/pom.xml index cd67962..8c8ad16 100644 --- a/integration-tests-support/pom.xml +++ b/integration-tests-support/pom.xml @@ -45,6 +45,7 @@ <module>test-support</module> <module>testcontainers-support</module> <module>mock-backend</module> + <module>wiremock</module> </modules> </project> diff --git a/integration-tests-support/wiremock/README.adoc b/integration-tests-support/wiremock/README.adoc new file mode 100644 index 0000000..0b477e7 --- /dev/null +++ b/integration-tests-support/wiremock/README.adoc @@ -0,0 +1,48 @@ +== WireMock test support + +This module provides test support for http://wiremock.org/[WireMock]. This enables the HTTP interactions between Camel & third party services to be +stubbed, recorded & replayed. + +=== Usage + +Add the following test scoped dependency into the extension integration test pom.xml: + +[source,xml] +---- +<dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-integration-wiremock-support</artifactId> + <scope>test</scope> +</dependency> +---- + +Next create a class that extends the abstract `WireMockTestResourceLifecycleManager`. You'll need to implement abstract methods: + +* `getRecordTargetBaseUrl` - To specify the base URL of the service interactions to be recorded +* `isMockingEnabled` - To determine whether the test should start the mock server or invoke the real service + +You can also override the `start` method to perform custom initialization logic and return additional configuration properties that Camel components may need. + +`WireMockTestResourceLifecycleManager` sets a system property named `wiremock.url`, which is the base URL to the running WireMock server. +In playback mode, you'll need to configure the Camel component under test to direct its API calls to this URL. + +==== Recording HTTP interactions + +The fundamentals of WireMock record and playback are documented http://wiremock.org/docs/record-playback/[here]. Setup of the `WireMockServer` is already handled by +`WireMockTestResourceLifecycleManager`. All you need to do is ensure directory `src/test/resources/mappings` exists and to trigger recording by either: + +System property `-Dwiremock.record=true` + +Or + +Environment variable `WIREMOCK_RECORD=true` + +When all tests complete, the recorded HTTP interactions will show up in the 'mappings' directory. The recorded stub file names are quite complex, feel free +to update them to something more human friendly. + +By default, stub mapping files are not saved when requests return an unsuccessful response code. You can alter this behaviour by overriding method `isDeleteRecordedMappingsOnError`. + +It's important to inspect the recorded files for the presence of any real API keys, secrets or passwords and replace them with made up values. + +WireMock generates new stub files on each recording, so it's a good idea to remove the existing contents from the 'mappings' directory +before a recording run. diff --git a/integration-tests-support/wiremock/pom.xml b/integration-tests-support/wiremock/pom.xml new file mode 100644 index 0000000..320ee44 --- /dev/null +++ b/integration-tests-support/wiremock/pom.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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. + +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-integration-tests-support</artifactId> + <version>1.4.0-SNAPSHOT</version> + <relativePath>../pom.xml</relativePath> + </parent> + <modelVersion>4.0.0</modelVersion> + + <artifactId>camel-quarkus-integration-wiremock-support</artifactId> + <name>Camel Quarkus :: Integration Tests :: WireMock :: Support</name> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-bom-test</artifactId> + <version>${project.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> + + <dependencies> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-integration-test-support</artifactId> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-integration-test-support-mock-backend</artifactId> + </dependency> + <dependency> + <groupId>com.github.tomakehurst</groupId> + <artifactId>wiremock-jre8</artifactId> + </dependency> + <dependency> + <groupId>org.jboss.logging</groupId> + <artifactId>jboss-logging</artifactId> + </dependency> + </dependencies> +</project> diff --git a/integration-tests-support/wiremock/src/main/java/org/apache/camel/quarkus/test/wiremock/MockServer.java b/integration-tests-support/wiremock/src/main/java/org/apache/camel/quarkus/test/wiremock/MockServer.java new file mode 100644 index 0000000..b6f29f1 --- /dev/null +++ b/integration-tests-support/wiremock/src/main/java/org/apache/camel/quarkus/test/wiremock/MockServer.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.test.wiremock; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used by the companion WireMockTestResourceLifecycleManager to inject + * {@link com.github.tomakehurst.wiremock.WireMockServer} instances into + * test classes. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface MockServer { +} diff --git a/integration-tests-support/wiremock/src/main/java/org/apache/camel/quarkus/test/wiremock/WireMockTestResourceLifecycleManager.java b/integration-tests-support/wiremock/src/main/java/org/apache/camel/quarkus/test/wiremock/WireMockTestResourceLifecycleManager.java new file mode 100644 index 0000000..dc64c87 --- /dev/null +++ b/integration-tests-support/wiremock/src/main/java/org/apache/camel/quarkus/test/wiremock/WireMockTestResourceLifecycleManager.java @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.quarkus.test.wiremock; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.recording.RecordingStatus; +import com.github.tomakehurst.wiremock.recording.SnapshotRecordResult; +import com.github.tomakehurst.wiremock.stubbing.StubMapping; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import org.apache.camel.quarkus.test.mock.backend.MockBackendUtils; +import org.jboss.logging.Logger; + +import static com.github.tomakehurst.wiremock.client.WireMock.recordSpec; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + +public abstract class WireMockTestResourceLifecycleManager implements QuarkusTestResourceLifecycleManager { + + protected static final Logger LOG = Logger.getLogger(WireMockTestResourceLifecycleManager.class); + protected WireMockServer server; + + /** + * Starts the {@link WireMockServer} and configures request / response recording if it has been enabled + */ + @Override + public Map<String, String> start() { + Map<String, String> properties = new HashMap<>(); + + if (isMockingEnabled() || isRecordingEnabled()) { + server = createServer(); + server.start(); + + if (isRecordingEnabled()) { + String recordTargetBaseUrl = getRecordTargetBaseUrl(); + if (recordTargetBaseUrl != null) { + LOG.infof("Enabling WireMock recording for %s", recordTargetBaseUrl); + server.startRecording(recordSpec() + .forTarget(recordTargetBaseUrl) + .allowNonProxied(false)); + } else { + throw new IllegalStateException( + "Must return a non-null value from getRecordTargetBaseUrl() in order to support WireMock recording"); + } + } + + String wireMockUrl = "http://localhost:" + server.port(); + LOG.infof("WireMock started on %s", wireMockUrl); + properties.put("wiremock.url", wireMockUrl); + } + + return properties; + } + + /** + * Stops the {@link WireMockServer} instance if it was started and stops recording if record mode was enabled + */ + @Override + public void stop() { + if (server != null) { + LOG.info("Stopping WireMockServer"); + if (server.getRecordingStatus().getStatus().equals(RecordingStatus.Recording)) { + LOG.info("Stopping recording"); + SnapshotRecordResult recordResult = server.stopRecording(); + + List<StubMapping> stubMappings = recordResult.getStubMappings(); + if (isDeleteRecordedMappingsOnError()) { + for (StubMapping mapping : stubMappings) { + int status = mapping.getResponse().getStatus(); + if (status >= 300 && mapping.shouldBePersisted()) { + try { + String fileName = mapping.getName() + "-" + mapping.getId() + ".json"; + Path mappingFilePath = Paths.get("./src/test/resources/mappings/", fileName); + Files.deleteIfExists(mappingFilePath); + LOG.infof("Deleted mapping file %s as status code was %d", fileName, status); + } catch (IOException e) { + LOG.errorf("Failed to delete mapping file %s", e, mapping.getName()); + } + } + } + } + } + server.stop(); + } + } + + /** + * If mocking is enabled, inject an instance of {@link WireMockServer} into any fields + * annotated with {@link MockServer}. This gives full control over creating recording rules + * and some aspects of the server lifecycle. + * + * The server instance is not injected if mocking is explicitly disabled, and therefore the resulting + * {@link MockServer} annotated field value will be null. + */ + @Override + public void inject(Object testInstance) { + if (isMockingEnabled() || isRecordingEnabled()) { + Class<?> c = testInstance.getClass(); + for (Field field : c.getDeclaredFields()) { + if (field.getAnnotation(MockServer.class) != null) { + if (!WireMockServer.class.isAssignableFrom(field.getType())) { + throw new RuntimeException("@MockServer can only be used on fields of type WireMockServer"); + } + + field.setAccessible(true); + try { + if (server == null) { + server = createServer(); + server.start(); + } + LOG.infof("Injecting WireMockServer for field %s", field.getName()); + field.set(testInstance, server); + return; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + } + } + + /** + * Determines whether each of the given environment variable names is set + */ + protected boolean envVarsPresent(String... envVarNames) { + if (envVarNames.length == 0) { + throw new IllegalArgumentException("envVarNames must not be empty"); + } + + boolean present = true; + for (String envVar : envVarNames) { + if (System.getenv(envVar) == null) { + present = false; + break; + } + } + + return present; + } + + /** + * Get the value of a given environment variable or a default value if it does not exist + */ + protected String envOrDefault(String envVarName, String defaultValue) { + String value = System.getenv(envVarName); + return value != null ? value : defaultValue; + } + + /** + * Whether recorded stub mapping files should be deleted if the HTTP response was an error code (>= 400). + * + * By default this returns true. Can be overridden if an error response is desired / expected from the HTTP request. + */ + protected boolean isDeleteRecordedMappingsOnError() { + return true; + } + + /** + * The target base URL that WireMock should watch for when recording requests. + * + * For example, if a test triggers an HTTP call on an external endpoint like https://api.foo.com/some/resource. + * Then the base URL would be https://api.foo.com + */ + protected abstract String getRecordTargetBaseUrl(); + + /** + * Conditions under which the {@link WireMockServer} should be started. + */ + protected abstract boolean isMockingEnabled(); + + /** + * Creates and starts a {@link WireMockServer} on a random port. {@link MockBackendUtils} triggers the log + * message that signifies mocking is in use. + */ + private WireMockServer createServer() { + LOG.info("Starting WireMockServer"); + MockBackendUtils.startMockBackend(true); + return new WireMockServer(options().dynamicPort()); + } + + /** + * Determine whether to enable WireMock record mode: + * + * http://wiremock.org/docs/record-playback/ + */ + private boolean isRecordingEnabled() { + String recordEnabled = System.getProperty("wiremock.record", System.getenv("WIREMOCK_RECORD")); + return recordEnabled != null && recordEnabled.equals("true"); + } +} diff --git a/pom.xml b/pom.xml index 892e91d..5274b94 100644 --- a/pom.xml +++ b/pom.xml @@ -119,6 +119,7 @@ <sshd.version>2.3.0</sshd.version> <stax2.version>4.2</stax2.version> <testcontainers.version>1.14.3</testcontainers.version> + <wiremock.version>2.27.2</wiremock.version> <zt-exec.version>1.11</zt-exec.version> <!-- Maven plugin versions (keep sorted alphabetically) --> diff --git a/poms/bom-test/pom.xml b/poms/bom-test/pom.xml index 1d970b6..1ffb844 100644 --- a/poms/bom-test/pom.xml +++ b/poms/bom-test/pom.xml @@ -98,6 +98,11 @@ <artifactId>camel-quarkus-integration-testcontainers-support</artifactId> <version>${camel-quarkus.version}</version> </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-integration-wiremock-support</artifactId> + <version>${camel-quarkus.version}</version> + </dependency> <dependency> <groupId>org.apache.ftpserver</groupId> @@ -166,6 +171,29 @@ </exclusions> </dependency> <dependency> + <groupId>com.github.tomakehurst</groupId> + <artifactId>wiremock-jre8</artifactId> + <version>${wiremock.version}</version> + <exclusions> + <exclusion> + <groupId>javax.xml.bind</groupId> + <artifactId>jaxb-api</artifactId> + </exclusion> + <exclusion> + <groupId>org.checkerframework</groupId> + <artifactId>checker-qual</artifactId> + </exclusion> + <exclusion> <!-- fix dependencyConvergence clash with junit-jupiter --> + <groupId>org.opentest4j</groupId> + <artifactId>opentest4j</artifactId> + </exclusion> + <exclusion> + <groupId>com.google.code.findbugs</groupId> + <artifactId>jsr305</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> <groupId>org.zeroturnaround</groupId> <artifactId>zt-exec</artifactId> <version>${zt-exec.version}</version>
