diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/MappingGenerator.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/MappingGenerator.java index 72a12d73f8..ee6a46ef59 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/MappingGenerator.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/MappingGenerator.java @@ -43,7 +43,7 @@ final class MappingGenerator { static Collection toMappings(File contractFile, Collection contracts, File mappingsFolder) { StubGeneratorProvider provider = new StubGeneratorProvider(); - Collection stubGenerators = provider.converterForName(contractFile.getName()); + Collection stubGenerators = provider.converterForName(contractFile); if (log.isDebugEnabled()) { log.debug("Found following matching stub generators " + stubGenerators); } diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerFactory.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerFactory.java index 089cb2a7b7..61d6bb32d9 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerFactory.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerFactory.java @@ -26,16 +26,19 @@ import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.cloud.contract.stubrunner.provider.wiremock.WireMockHttpServerStub; import org.springframework.cloud.contract.verifier.converter.RecursiveFilesConverter; import org.springframework.cloud.contract.verifier.converter.StubGenerator; import org.springframework.cloud.contract.verifier.converter.StubGeneratorProvider; import org.springframework.cloud.contract.verifier.messaging.MessageVerifier; import org.springframework.core.io.Resource; +import org.springframework.core.io.support.SpringFactoriesLoader; /** * Factory of StubRunners. Basing on the options and passed collaborators downloads the @@ -109,6 +112,12 @@ class StubRunnerFactory { } private void removeCurrentMappings(Path path) { + + List httpServerStubs = SpringFactoriesLoader.loadFactories(HttpServerStub.class, null); + if (httpServerStubs.isEmpty()) { + httpServerStubs.add(new WireMockHttpServerStub()); + } + try { Files.walkFileTree(path, new SimpleFileVisitor() { @@ -116,13 +125,15 @@ class StubRunnerFactory { private final StubGeneratorProvider provider = new StubGeneratorProvider(); + private final HttpServerStub wireMockHttpServerStub = new WireMockHttpServerStub(); + @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - Collection stubGenerators = this.provider.converterForName(file.toString()); - if (!stubGenerators.isEmpty()) { + File fileToConvert = file.toFile(); + Collection stubGenerators = this.provider.converterForName(fileToConvert); + if (!stubGenerators.isEmpty() || this.wireMockHttpServerStub.isAccepted(fileToConvert)) { if (log.isDebugEnabled()) { - log.debug("Deleting file [" + file.toString() - + "] since at least one stub generator would run it"); + log.debug("Deleting file [" + file.toString() + "] since it contains a valid mapping."); } try { Files.delete(file); diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStub.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStub.java index 67ea1f4323..036239c3a3 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStub.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStub.java @@ -31,6 +31,7 @@ import java.util.concurrent.ConcurrentHashMap; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.common.JsonException; import com.github.tomakehurst.wiremock.common.Slf4jNotifier; import com.github.tomakehurst.wiremock.core.WireMockConfiguration; import com.github.tomakehurst.wiremock.extension.Extension; @@ -232,14 +233,24 @@ public class WireMockHttpServerStub implements HttpServerStub { @Override public boolean isAccepted(File file) { - return file.getName().endsWith(".json"); + return file.getName().endsWith(".json") && validMapping(file); + } + + private boolean validMapping(File file) { + try { + getMapping(file); + return true; + } + catch (IllegalStateException e) { + return false; + } } StubMapping getMapping(File file) { try (InputStream stream = Files.newInputStream(file.toPath())) { return StubMapping.buildFrom(StreamUtils.copyToString(stream, Charset.forName("UTF-8"))); } - catch (IOException e) { + catch (IOException | JsonException e) { throw new IllegalStateException("Cannot read file", e); } } diff --git a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerSpec.groovy b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerSpec.groovy index d583f78907..d6913e2a17 100644 --- a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerSpec.groovy +++ b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/StubRunnerSpec.groovy @@ -57,22 +57,48 @@ class StubRunnerSpec extends Specification { runner.close() } - def 'should generate stubs at runtime'() { given: Arguments args = argumentsWithGenerateStubs() - StubDownloader downloader = new FileStubDownloader().build(args.stubRunnerOptions); + StubDownloader downloader = new FileStubDownloader().build(args.stubRunnerOptions) StubRunner runner = new StubRunnerFactory(args.stubRunnerOptions, downloader, new NoOpStubMessages()).createStubsFromServiceConfiguration().first() when: runner.runStubs() then: URL url = runner.findStubUrl("groupId2", "artifactId2") - new URL(url.toString() + "/goodbye").text + "Goodbye World!" == new URL(url.toString() + "/goodbye").text + cleanup: + runner.close() + } + + def 'should override existing mappings when generating stubs at runtime'() { + given: + Arguments args = argumentsWithGenerateStubs() + StubDownloader downloader = new FileStubDownloader().build(args.stubRunnerOptions) + StubRunner runner = new StubRunnerFactory(args.stubRunnerOptions, + downloader, new NoOpStubMessages()).createStubsFromServiceConfiguration().first() when: - new URL(url.toString() + "/hello").text + runner.runStubs() then: - thrown(FileNotFoundException) + URL url = runner.findStubUrl("groupId2", "artifactId2") + // don't return the response defined in hello.json, but the one defined in the contract + "Hello New World!" == new URL(url.toString() + "/hello").text + cleanup: + runner.close() + } + + def 'should handle contracts with body contents loaded from external file when generating stubs at runtime'() { + given: + Arguments args = argumentsWithGenerateStubs() + StubDownloader downloader = new FileStubDownloader().build(args.stubRunnerOptions) + StubRunner runner = new StubRunnerFactory(args.stubRunnerOptions, + downloader, new NoOpStubMessages()).createStubsFromServiceConfiguration().first() + when: + runner.runStubs() + then: + URL url = runner.findStubUrl("groupId2", "artifactId2") + "Goodbye from file!" == new URL(url.toString() + "/goodbye_from_file").text cleanup: runner.close() } diff --git a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/provider/wiremock/TestWireMockExtensions.groovy b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/provider/wiremock/TestWireMockExtensions.groovy index 91b1a5293e..bb69a4b1fc 100644 --- a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/provider/wiremock/TestWireMockExtensions.groovy +++ b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/provider/wiremock/TestWireMockExtensions.groovy @@ -21,8 +21,10 @@ import com.github.tomakehurst.wiremock.extension.Extension import com.github.tomakehurst.wiremock.extension.Parameters import com.github.tomakehurst.wiremock.extension.ResponseTransformer import com.github.tomakehurst.wiremock.http.ChunkedDribbleDelay +import com.github.tomakehurst.wiremock.http.HttpHeader import com.github.tomakehurst.wiremock.http.Request import com.github.tomakehurst.wiremock.http.Response +import wiremock.org.apache.http.HttpHeaders import org.springframework.cloud.contract.verifier.dsl.wiremock.DefaultResponseTransformer import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockExtensions @@ -52,13 +54,13 @@ class CustomExtension extends ResponseTransformer { } /** - * Transformer returns the "surprise!" body regardless of what you - * the stub mapping returns + * Transformer adds a X-My-Header with value "surprise!" to the response */ @Override Response transform(Request request, Response response, FileSource files, Parameters parameters) { + def headers = response.headers + new HttpHeader("X-My-Header", "surprise!") return new Response(response.status, response.statusMessage, - "surprise!", response.headers, response.wasConfigured(), response.fault, + response.body, headers, response.wasConfigured(), response.fault, response.initialDelay, new ChunkedDribbleDelay(0, 0), response.fromProxy) } diff --git a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStubSpec.groovy b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStubSpec.groovy index e73b68ca8d..e937068f24 100644 --- a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStubSpec.groovy +++ b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStubSpec.groovy @@ -23,11 +23,16 @@ import spock.lang.Specification import org.springframework.boot.test.system.OutputCaptureRule import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.http.HttpEntity +import org.springframework.http.HttpMethod import org.springframework.web.client.RestTemplate class WireMockHttpServerStubSpec extends Specification { public static final File MAPPING_DESCRIPTOR = new File('src/test/resources/transformers.json') + public static final File ARBITRARY_JSON = new File('src/test/resources/sample_response.json') + public static final File PDF = new File('src/test/resources/request.pdf') + public static final File BROKEN_MAPPING = new File('src/test/resources/broken.json') @Rule OutputCaptureRule capture = new OutputCaptureRule() @@ -50,7 +55,9 @@ class WireMockHttpServerStubSpec extends Specification { then: noExceptionThrown() expect: - "surprise!" == new RestTemplate().getForObject("http://localhost:" + mappingDescriptor.port() + "/ping", String.class) + URI uri = new URI("http://localhost:" + mappingDescriptor.port() + "/ping") + "surprise!" == new RestTemplate().exchange(uri, HttpMethod.GET, (HttpEntity)null, String.class) + .getHeaders().getFirst("X-My-Header") cleanup: mappingDescriptor?.stop() } @@ -72,4 +79,40 @@ class WireMockHttpServerStubSpec extends Specification { cleanup: mappingDescriptor?.stop() } + + def 'should accept a valid mapping'() { + given: + WireMockHttpServerStub httpServerStub = new WireMockHttpServerStub() + when: + boolean accepted = httpServerStub.isAccepted(MAPPING_DESCRIPTOR) + then: + accepted + } + + def 'should not accept an arbitrary JSON file'() { + given: + WireMockHttpServerStub httpServerStub = new WireMockHttpServerStub() + when: + boolean accepted = httpServerStub.isAccepted(ARBITRARY_JSON) + then: + !accepted + } + + def 'should not accept a broken mapping file'() { + given: + WireMockHttpServerStub httpServerStub = new WireMockHttpServerStub() + when: + boolean accepted = httpServerStub.isAccepted(BROKEN_MAPPING) + then: + !accepted + } + + def 'should not accept a non-JSON file'() { + given: + WireMockHttpServerStub httpServerStub = new WireMockHttpServerStub() + when: + boolean accepted = httpServerStub.isAccepted(PDF) + then: + !accepted + } } diff --git a/spring-cloud-contract-stub-runner/src/test/resources/arbitrary.json b/spring-cloud-contract-stub-runner/src/test/resources/arbitrary.json new file mode 100644 index 0000000000..35ecab0f56 --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/test/resources/arbitrary.json @@ -0,0 +1,12 @@ +[ + { + "id": "1", + "firstname": "John", + "name": "Doe" + }, + { + "id": "2", + "firstname": "Lisa", + "name": "Smith" + } +] \ No newline at end of file diff --git a/spring-cloud-contract-stub-runner/src/test/resources/broken.json b/spring-cloud-contract-stub-runner/src/test/resources/broken.json new file mode 100644 index 0000000000..3ac85b61ad --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/test/resources/broken.json @@ -0,0 +1,7 @@ +{ + "request": { + "method": "GET", + "url": "/ping" + }, + "response": { + "status": 200, diff --git a/spring-cloud-contract-stub-runner/src/test/resources/generateStubs/contracts/goodbye_from_file.groovy b/spring-cloud-contract-stub-runner/src/test/resources/generateStubs/contracts/goodbye_from_file.groovy new file mode 100644 index 0000000000..67c5325eed --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/test/resources/generateStubs/contracts/goodbye_from_file.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2020 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. + */ + +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + request { + url("/goodbye_from_file") + method(GET()) + } + response { + status(OK()) + body(file("goodbye_response.txt")) + } +} diff --git a/spring-cloud-contract-stub-runner/src/test/resources/generateStubs/contracts/goodbye_response.txt b/spring-cloud-contract-stub-runner/src/test/resources/generateStubs/contracts/goodbye_response.txt new file mode 100644 index 0000000000..02e35ff340 --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/test/resources/generateStubs/contracts/goodbye_response.txt @@ -0,0 +1 @@ +Goodbye from file! \ No newline at end of file diff --git a/spring-cloud-contract-stub-runner/src/test/resources/generateStubs/contracts/hello.groovy b/spring-cloud-contract-stub-runner/src/test/resources/generateStubs/contracts/hello.groovy new file mode 100644 index 0000000000..cb971a69ee --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/test/resources/generateStubs/contracts/hello.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2020 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. + */ + +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + request { + url("/hello") + method(GET()) + } + response { + status(OK()) + body("Hello New World!") + } +} diff --git a/spring-cloud-contract-stub-runner/src/test/resources/generateStubs/mappings/hello.json b/spring-cloud-contract-stub-runner/src/test/resources/generateStubs/contracts/hello.json similarity index 100% rename from spring-cloud-contract-stub-runner/src/test/resources/generateStubs/mappings/hello.json rename to spring-cloud-contract-stub-runner/src/test/resources/generateStubs/contracts/hello.json diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/RecursiveFilesConverter.java b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/RecursiveFilesConverter.java index c52121b6a7..8d14171a44 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/RecursiveFilesConverter.java +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/RecursiveFilesConverter.java @@ -120,8 +120,7 @@ public class RecursiveFilesConverter { } File sourceFile = contract.getPath().toFile(); Collection stubGenerators = contract.getConvertedContract() != null - ? holder.allOrDefault(new DslToWireMockClientConverter()) - : holder.converterForName(sourceFile.getAbsolutePath()); + ? holder.allOrDefault(new DslToWireMockClientConverter()) : holder.converterForName(sourceFile); try { String path = sourceFile.getPath(); if (excludeBuildFolders && (matchesPath(path, "target") || matchesPath(path, "build"))) { diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/StubGenerator.java b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/StubGenerator.java index 84a0cfaf98..3f26ff8efe 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/StubGenerator.java +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/StubGenerator.java @@ -16,6 +16,7 @@ package org.springframework.cloud.contract.verifier.converter; +import java.io.File; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -32,12 +33,12 @@ import org.springframework.cloud.contract.verifier.file.ContractMetadata; public interface StubGenerator { /** - * @param fileName - file name or absolute path of a contract - * @return {@code true} if the converter can handle the contract file to convert it - * into a stub. + * @param file - file + * @return {@code true} if the converter can handle the file to convert it into a + * stub. */ - default boolean canHandleFileName(String fileName) { - return true; + default boolean canHandleFileName(File file) { + return file.getName().endsWith(fileExtension()); } /** @@ -81,7 +82,7 @@ public interface StubGenerator { * @param inputFileName - name of the input file * @return the name of the converted stub file. If you have multiple contracts in a * single file then a prefix will be added to the generated file. If you provide the - * {@link Contract#getName()} field then that field will override the generated file + * {@link Contract#getName} field then that field will override the generated file * name. * * Example: name of file with 2 contracts is {@code foo.groovy}, it will be converted diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/StubGeneratorProvider.java b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/StubGeneratorProvider.java index edc7c7784b..1fbe8e4065 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/StubGeneratorProvider.java +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/StubGeneratorProvider.java @@ -16,6 +16,7 @@ package org.springframework.cloud.contract.verifier.converter; +import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -32,7 +33,7 @@ import org.springframework.core.io.support.SpringFactoriesLoader; */ public class StubGeneratorProvider { - private final List converters = new ArrayList(); + private final List converters = new ArrayList<>(); public StubGeneratorProvider() { this.converters.addAll(SpringFactoriesLoader.loadFactories(StubGenerator.class, null)); @@ -42,8 +43,8 @@ public class StubGeneratorProvider { this.converters.addAll(converters); } - public Collection converterForName(final String fileName) { - return this.converters.stream().filter(stubGenerator -> stubGenerator.canHandleFileName(fileName)) + public Collection converterForName(final File file) { + return this.converters.stream().filter(stubGenerator -> stubGenerator.canHandleFileName(file)) .collect(Collectors.toList()); } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockConverter.java b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockConverter.java index fc13a4d93f..c9dc4197a6 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockConverter.java +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/wiremock/DslToWireMockConverter.java @@ -17,11 +17,18 @@ package org.springframework.cloud.contract.verifier.wiremock; import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; import java.nio.file.Files; +import com.github.tomakehurst.wiremock.common.JsonException; import com.github.tomakehurst.wiremock.stubbing.StubMapping; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.cloud.contract.verifier.converter.StubGenerator; +import org.springframework.util.StreamUtils; /** * WireMock implementation of the {@link StubGenerator}. @@ -30,6 +37,8 @@ import org.springframework.cloud.contract.verifier.converter.StubGenerator; */ public abstract class DslToWireMockConverter implements StubGenerator { + private static final Log log = LogFactory.getLog(DslToWireMockConverter.class); + @Override public String generateOutputFileNameForInput(String inputFileName) { return inputFileName.replaceAll(extension(inputFileName), "json"); @@ -44,15 +53,18 @@ public abstract class DslToWireMockConverter implements StubGenerator