Merge remote-tracking branch 'origin/2.2.x'

This commit is contained in:
Marcin Grzejszczak
2020-11-10 15:35:41 +01:00
17 changed files with 216 additions and 33 deletions

View File

@@ -43,7 +43,7 @@ final class MappingGenerator {
static Collection<Path> toMappings(File contractFile, Collection<Contract> contracts, File mappingsFolder) {
StubGeneratorProvider provider = new StubGeneratorProvider();
Collection<StubGenerator> stubGenerators = provider.converterForName(contractFile.getName());
Collection<StubGenerator> stubGenerators = provider.converterForName(contractFile);
if (log.isDebugEnabled()) {
log.debug("Found following matching stub generators " + stubGenerators);
}

View File

@@ -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<HttpServerStub> httpServerStubs = SpringFactoriesLoader.loadFactories(HttpServerStub.class, null);
if (httpServerStubs.isEmpty()) {
httpServerStubs.add(new WireMockHttpServerStub());
}
try {
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
@@ -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<StubGenerator> stubGenerators = this.provider.converterForName(file.toString());
if (!stubGenerators.isEmpty()) {
File fileToConvert = file.toFile();
Collection<StubGenerator> 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);

View File

@@ -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);
}
}

View File

@@ -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()
}

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,12 @@
[
{
"id": "1",
"firstname": "John",
"name": "Doe"
},
{
"id": "2",
"firstname": "Lisa",
"name": "Smith"
}
]

View File

@@ -0,0 +1,7 @@
{
"request": {
"method": "GET",
"url": "/ping"
},
"response": {
"status": 200,

View File

@@ -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"))
}
}

View File

@@ -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!")
}
}

View File

@@ -120,8 +120,7 @@ public class RecursiveFilesConverter {
}
File sourceFile = contract.getPath().toFile();
Collection<StubGenerator> 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"))) {

View File

@@ -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<T> {
/**
* @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<T> {
* @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

View File

@@ -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<StubGenerator> converters = new ArrayList<StubGenerator>();
private final List<StubGenerator> 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<StubGenerator> converterForName(final String fileName) {
return this.converters.stream().filter(stubGenerator -> stubGenerator.canHandleFileName(fileName))
public Collection<StubGenerator> converterForName(final File file) {
return this.converters.stream().filter(stubGenerator -> stubGenerator.canHandleFileName(file))
.collect(Collectors.toList());
}

View File

@@ -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<StubMapping> {
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<StubMappin
}
@Override
public boolean canHandleFileName(String fileName) {
if (!fileName.endsWith(fileExtension())) {
public boolean canHandleFileName(File file) {
if (!file.getName().endsWith(fileExtension())) {
return false;
}
try {
StubMapping.buildFrom(new String(Files.readAllBytes(new File(fileName).toPath())));
try (InputStream stream = Files.newInputStream(file.toPath())) {
StubMapping.buildFrom(StreamUtils.copyToString(stream, Charset.forName("UTF-8")));
return true;
}
catch (Exception e) {
catch (IOException | JsonException e) {
if (log.isDebugEnabled()) {
log.debug("Cannot read file", e);
}
return false;
}
}

View File

@@ -193,8 +193,9 @@ org.springframework.cloud.contract.spec.Contract.make {
private StubGenerator stubGenerator(String stub) {
return new StubGenerator() {
@Override
boolean canHandleFileName(String fileName) {
boolean canHandleFileName(File fileName) {
return true
}