diff --git a/docs/src/main/asciidoc/spring-cloud-wiremock.adoc b/docs/src/main/asciidoc/spring-cloud-wiremock.adoc index be6783c4ea..f3104be932 100644 --- a/docs/src/main/asciidoc/spring-cloud-wiremock.adoc +++ b/docs/src/main/asciidoc/spring-cloud-wiremock.adoc @@ -25,10 +25,12 @@ To start the stub server on a different port use `@AutoConfigureWireMock(port=99 === Registering Stubs Automatically -If you add a `stubs` attribute to your `@AutoConfigureWireMock` then -it will register WireMock JSON stubs from the file system or -classpath. The stubs attribute can be a resource pattern (ant-style) -or a directory, in which case `**/*.json` is appended. Example: +If you use `@AutoConfigureWireMock` then it will register WireMock +JSON stubs from the file system or classpath, by default from +`file:src/test/resources/mappings`. You can customize the locations +using the `stubs` attribute in the annotation, which can be a resource +pattern (ant-style) or a directory, in which case `**/*.json` is +appended. Example: ---- @RunWith(SpringRunner.class) @@ -47,6 +49,30 @@ public class WiremockImportApplicationTests { } ---- +NOTE: Actually WireMock always loads mappings from +`src/test/resources/mappings` *as well as* the custom locations in the +stubs attribute. To change this behaviour you have to also specify a +files root as described next. + +=== Using Files to Specify the Stub Bodies + +WireMock can read response bodies from files on the classpath or file +system. In that case you will see in the JSON DSL that the response +has a "bodyFileName" instead of a (literal) "body". The files are +resolved relative to a root directory `src/test/resources/__files` by +default. To customize this location you can set the `files` attribute +in the `@AutoConfigureWireMock` annotation to the location of the +parent directory (i.e. the place where `__files` is a +subdirectory). You can use Spring resource notation to refer to +`file:...` or `classpath:...` locations (but generic URLs are not +supported). A list of values can be given and WireMock will resolve +the first file that exists when it needs to find a response body. + +NOTE: when you configure the `files` root, then it affects the +automatic loading of stubs as well (they come from the root location +in a subdirectory called "mappings"). The value of `files` has no +effect on the stubs loaded explicitly from the `stubs` attribute. + === Alternative: Using JUnit Rules For a more conventional WireMock experience, using JUnit `@Rules` to diff --git a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMock.java b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMock.java index e78e3bb9bd..fb8db93eb1 100644 --- a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMock.java +++ b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMock.java @@ -45,6 +45,8 @@ public @interface AutoConfigureWireMock { int httpsPort() default -1; - String stubs() default ""; + String[] stubs() default {""}; + + String[] files() default {""}; } diff --git a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockConfiguration.java b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockConfiguration.java index 43d9cfb621..5370abcead 100644 --- a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockConfiguration.java +++ b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockConfiguration.java @@ -18,6 +18,8 @@ package org.springframework.cloud.contract.wiremock; import java.io.IOException; import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; import javax.annotation.PostConstruct; @@ -25,6 +27,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.contract.wiremock.file.ResourcesFileSource; import org.springframework.context.SmartLifecycle; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; @@ -72,14 +75,14 @@ public class WireMockConfiguration implements SmartLifecycle { @PostConstruct public void init() throws IOException { if (this.options == null) { - com.github.tomakehurst.wiremock.core.WireMockConfiguration factory = WireMockSpring - .options(); + com.github.tomakehurst.wiremock.core.WireMockConfiguration factory = WireMockSpring.options(); if (this.wireMock.getPort() != 8080) { factory.port(this.wireMock.getPort()); } if (this.wireMock.getHttpsPort() != -1) { factory.httpsPort(this.wireMock.getHttpsPort()); } + registerFiles(factory); this.options = factory; } this.server = new WireMockServer(this.options); @@ -90,19 +93,42 @@ public class WireMockConfiguration implements SmartLifecycle { } private void registerStubs() throws IOException { - if (StringUtils.hasText(this.wireMock.getStubs())) { - PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver( - this.resourceLoader); - String pattern = this.wireMock.getStubs(); - if (!pattern.contains("*")) { + for (String stubs : this.wireMock.getStubs()) { + if (StringUtils.hasText(stubs)) { + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver( + this.resourceLoader); + String pattern = stubs; + if (!pattern.contains("*")) { + if (!pattern.endsWith("/")) { + pattern = pattern + "/"; + } + pattern = pattern + "**/*.json"; + } + for (Resource resource : resolver.getResources(pattern)) { + this.server.addStubMapping(StubMapping + .buildFrom(StreamUtils.copyToString(resource.getInputStream(), Charset.forName("UTF-8")))); + } + } + } + } + + private void registerFiles(com.github.tomakehurst.wiremock.core.WireMockConfiguration factory) throws IOException { + for (String files : this.wireMock.getFiles()) { + if (StringUtils.hasText(files)) { + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver( + this.resourceLoader); + String pattern = files; if (!pattern.endsWith("/")) { pattern = pattern + "/"; } - pattern = pattern + "**/*.json"; - } - for (Resource resource : resolver.getResources(pattern)) { - this.server.addStubMapping(StubMapping.buildFrom(StreamUtils.copyToString( - resource.getInputStream(), Charset.forName("UTF-8")))); + List resources = new ArrayList<>(); + for (Resource resource : resolver.getResources(pattern)) { + if (resource.exists()) { + resources.add(resource); + } + } + ResourcesFileSource fileSource = new ResourcesFileSource(resources.toArray(new Resource[0])); + factory.fileSource(fileSource); } } } @@ -151,7 +177,9 @@ class WireMockProperties { private int httpsPort = -1; - private String stubs; + private String[] stubs; + + private String[] files; public int getPort() { return this.port; @@ -169,12 +197,20 @@ class WireMockProperties { this.httpsPort = httpsPort; } - public String getStubs() { + public String[] getStubs() { return this.stubs; } - public void setStubs(String stubs) { + public void setStubs(String[] stubs) { this.stubs = stubs; } + public String[] getFiles() { + return this.files; + } + + public void setFiles(String[] files) { + this.files = files; + } + } \ No newline at end of file diff --git a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockRestServiceServer.java b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockRestServiceServer.java index e4e94fe93a..ab5fa90719 100644 --- a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockRestServiceServer.java +++ b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockRestServiceServer.java @@ -232,7 +232,7 @@ public class WireMockRestServiceServer { if (!location.endsWith("/")) { location = location + "/"; } - for (Resource files : resolver.getResources(location)) { + for (Resource files : this.resolver.getResources(location)) { if (files.exists()) { try { Resource resource = files.createRelative(file); diff --git a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/file/ResourcesFileSource.java b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/file/ResourcesFileSource.java new file mode 100644 index 0000000000..a9a5a3d849 --- /dev/null +++ b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/file/ResourcesFileSource.java @@ -0,0 +1,159 @@ +/* + * Copyright 2015-2016 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 + * + * 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.springframework.cloud.contract.wiremock.file; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; + +import com.github.tomakehurst.wiremock.common.BinaryFile; +import com.github.tomakehurst.wiremock.common.ClasspathFileSource; +import com.github.tomakehurst.wiremock.common.FileSource; +import com.github.tomakehurst.wiremock.common.SingleRootFileSource; +import com.github.tomakehurst.wiremock.common.TextFile; + +/** + * @author Dave Syer + * + */ +public class ResourcesFileSource implements FileSource { + + private final FileSource[] sources; + + public ResourcesFileSource(Resource[] resources) { + this.sources = new FileSource[resources.length]; + for (int i = 0; i < resources.length; i++) { + Resource resource = resources[i]; + if (resource instanceof ClassPathResource) { + ClassPathResource classes = (ClassPathResource) resource; + this.sources[i] = new ClasspathFileSource(classes.getPath()); + } + else if (resource instanceof FileSystemResource) { + FileSystemResource files = (FileSystemResource) resource; + this.sources[i] = new SingleRootFileSource(files.getFile()); + } + else { + throw new IllegalArgumentException("Unsupported resource type for file source: " + resource.getClass()); + } + } + } + + @Override + public BinaryFile getBinaryFileNamed(String name) { + for (FileSource resource : this.sources) { + try { + UrlResource uri = new UrlResource(resource.getBinaryFileNamed(name).getUri()); + if (uri.exists()) { + return resource.getBinaryFileNamed(name); + } + } + catch (IOException e) { + // Ignore + } + } + throw new IllegalStateException("Cannot create file for " + name); + } + + @Override + public void createIfNecessary() { + throw new UnsupportedOperationException("Resource file sources are read-only"); + } + + @Override + public FileSource child(String subDirectoryName) { + for (FileSource resource : this.sources) { + try { + UrlResource uri = new UrlResource(resource.child(subDirectoryName).getUri()); + if (uri.createRelative(subDirectoryName).exists()) { + return resource.child(subDirectoryName); + } + } + catch (IOException e) { + // Ignore + } + } + return this.sources[0].child(subDirectoryName); + } + + @Override + public String getPath() { + for (FileSource resource : this.sources) { + try { + UrlResource uri = new UrlResource(resource.getUri()); + if (uri.exists()) { + return resource.getPath(); + } + } + catch (IOException e) { + // Ignore + } + } + return this.sources[0].getPath(); + } + + @Override + public URI getUri() { + for (FileSource resource : this.sources) { + try { + UrlResource uri = new UrlResource(resource.getUri()); + if (uri.exists()) { + return resource.getUri(); + } + } + catch (IOException e) { + // Ignore + } + } + return this.sources[0].getUri(); + } + + @Override + public List listFilesRecursively() { + List files = new ArrayList<>(); + for (FileSource resource : this.sources) { + files.addAll(resource.listFilesRecursively()); + } + return files; + } + + @Override + public void writeTextFile(String name, String contents) { + throw new UnsupportedOperationException("Resource file sources are read-only"); + } + + @Override + public void writeBinaryFile(String name, byte[] contents) { + throw new UnsupportedOperationException("Resource file sources are read-only"); + } + + @Override + public boolean exists() { + for (FileSource resource : this.sources) { + if (resource.exists()) { + return true; + } + } + return false; + } + +} diff --git a/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockFilesApplicationTests.java b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockFilesApplicationTests.java new file mode 100644 index 0000000000..0ef3869b08 --- /dev/null +++ b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockFilesApplicationTests.java @@ -0,0 +1,27 @@ +package org.springframework.cloud.contract.wiremock; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes=WiremockTestsApplication.class, properties="app.baseUrl=http://localhost:${wiremock.server.port}", webEnvironment=WebEnvironment.NONE) +@DirtiesContext +@AutoConfigureWireMock(port=0, files="classpath:/root/") +public class AutoConfigureWireMockFilesApplicationTests { + + @Autowired + private Service service; + + @Test + public void contextLoads() throws Exception { + assertThat(this.service.go()).isEqualTo("{\"message\":\"Hello Root\"}"); + } + +} diff --git a/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockStubsAndFilesApplicationTests.java b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockStubsAndFilesApplicationTests.java new file mode 100644 index 0000000000..28f96987ab --- /dev/null +++ b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockStubsAndFilesApplicationTests.java @@ -0,0 +1,27 @@ +package org.springframework.cloud.contract.wiremock; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes=WiremockTestsApplication.class, properties="app.baseUrl=http://localhost:${wiremock.server.port}", webEnvironment=WebEnvironment.NONE) +@DirtiesContext +@AutoConfigureWireMock(port=0, files="classpath:/root/", stubs="file:src/test/resources/io.stubs/mappings") +public class AutoConfigureWireMockStubsAndFilesApplicationTests { + + @Autowired + private Service service; + + @Test + public void contextLoads() throws Exception { + assertThat(this.service.go()).isEqualTo("{\"message\":\"Hello Root\"}"); + } + +} diff --git a/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockStubsAndMultipleFilesApplicationTests.java b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockStubsAndMultipleFilesApplicationTests.java new file mode 100644 index 0000000000..de7e88d1d8 --- /dev/null +++ b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockStubsAndMultipleFilesApplicationTests.java @@ -0,0 +1,27 @@ +package org.springframework.cloud.contract.wiremock; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes=WiremockTestsApplication.class, properties="app.baseUrl=http://localhost:${wiremock.server.port}", webEnvironment=WebEnvironment.NONE) +@DirtiesContext +@AutoConfigureWireMock(port=0, files={"classpath:/nonexistent/", "classpath:/root/"}, stubs="file:src/test/resources/io.stubs/mappings") +public class AutoConfigureWireMockStubsAndMultipleFilesApplicationTests { + + @Autowired + private Service service; + + @Test + public void contextLoads() throws Exception { + assertThat(this.service.go()).isEqualTo("{\"message\":\"Hello Root\"}"); + } + +} diff --git a/spring-cloud-contract-wiremock/src/test/resources/files/__files/hello.json b/spring-cloud-contract-wiremock/src/test/resources/files/__files/hello.json new file mode 100644 index 0000000000..89c1a639d8 --- /dev/null +++ b/spring-cloud-contract-wiremock/src/test/resources/files/__files/hello.json @@ -0,0 +1 @@ +{"message":"Hello Root"} \ No newline at end of file diff --git a/spring-cloud-contract-wiremock/src/test/resources/root/__files/hello.json b/spring-cloud-contract-wiremock/src/test/resources/root/__files/hello.json new file mode 100644 index 0000000000..89c1a639d8 --- /dev/null +++ b/spring-cloud-contract-wiremock/src/test/resources/root/__files/hello.json @@ -0,0 +1 @@ +{"message":"Hello Root"} \ No newline at end of file diff --git a/spring-cloud-contract-wiremock/src/test/resources/root/mappings/resource-with-body-file.json b/spring-cloud-contract-wiremock/src/test/resources/root/mappings/resource-with-body-file.json new file mode 100644 index 0000000000..81c1479a97 --- /dev/null +++ b/spring-cloud-contract-wiremock/src/test/resources/root/mappings/resource-with-body-file.json @@ -0,0 +1,10 @@ +{ + "request" : { + "urlPath" : "/test", + "method" : "GET" + }, + "response" : { + "status" : 200, + "bodyFileName" : "hello.json" + } +} \ No newline at end of file