Merge branch '1.0.x'

This commit is contained in:
Marcin Grzejszczak
2017-02-23 17:37:40 +01:00
11 changed files with 550 additions and 3 deletions

View File

@@ -0,0 +1,131 @@
package org.springframework.cloud.contract.wiremock.restdocs;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.http.HttpHeaders;
import org.springframework.restdocs.RestDocumentationContext;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.restdocs.templates.TemplateEngine;
/**
* A {@link org.springframework.restdocs.snippet.Snippet} that documents the Spring Cloud Contract Groovy DSL.
*
* @author Marcin Grzejszczak
* @since 1.0.4
*/
public class ContractDslSnippet extends TemplatedSnippet {
private static final String CONTRACTS_FOLDER = "contracts";
private static final String SNIPPET_NAME = "dsl-contract";
private Map<String, Object> model = new HashMap<>();
/**
* Creates a new {@code ContractDslSnippet} with no additional attributes.
*/
protected ContractDslSnippet() {
this(null);
}
/**
* Creates a new {@code ContractDslSnippet} with the given additional
* {@code attributes} that will be included in the model during template rendering.
*
* @param attributes The additional attributes
*/
protected ContractDslSnippet(Map<String, Object> attributes) {
super(SNIPPET_NAME, attributes);
}
@Override
protected Map<String, Object> createModel(Operation operation) {
return this.model;
}
@Override public void document(Operation operation) throws IOException {
TemplateEngine templateEngine = (TemplateEngine) operation.getAttributes().get(TemplateEngine.class.getName());
String renderedContract = templateEngine.compileTemplate("default-dsl-contract-only")
.render(createModelForContract(operation));
this.model.put("contract", renderedContract);
storeDslContract(operation, renderedContract);
super.document(operation);
}
private void insertResponseModel(Operation operation, Map<String, Object> model) {
OperationResponse response = operation.getResponse();
model.put("response_status", response.getStatus().value());
model.put("response_body_present", response.getContent().length > 0);
model.put("response_body", response.getContentAsString());
HttpHeaders headers = response.getHeaders();
model.put("response_headers_present", !headers.isEmpty());
model.put("response_headers", headers.toSingleValueMap().entrySet());
@SuppressWarnings("unchecked") Set<String> jsonPaths = (Set<String>) operation.getAttributes()
.get("contract.jsonPaths");
model.put("response_json_paths_present", jsonPaths != null && !jsonPaths.isEmpty());
model.put("response_json_paths", jsonPaths(jsonPaths));
}
private Set<JsonPaths> jsonPaths(Set<String> jsonPaths) {
Set<JsonPaths> paths = new HashSet<>();
if (jsonPaths == null) {
return paths;
}
for (String s : jsonPaths) {
paths.add(new JsonPaths(s));
}
return paths;
}
private void insertRequestModel(Operation operation, Map<String, Object> model) {
OperationRequest request = operation.getRequest();
model.put("request_method", request.getMethod());
model.put("request_url", request.getUri());
model.put("request_body_present", request.getContent().length > 0);
model.put("request_body", request.getContentAsString());
HttpHeaders headers = request.getHeaders();
model.put("request_headers_present", !headers.isEmpty());
model.put("request_headers", headers.toSingleValueMap().entrySet());
}
private Map<String, Object> createModelForContract(Operation operation) {
Map<String, Object> modelForContract = new HashMap<>();
insertRequestModel(operation, modelForContract);
insertResponseModel(operation, modelForContract);
return modelForContract;
}
private void storeDslContract(Operation operation, String content)
throws IOException {
RestDocumentationContext context = (RestDocumentationContext) operation
.getAttributes().get(RestDocumentationContext.class.getName());
File output = new File(context.getOutputDirectory(),
CONTRACTS_FOLDER + "/" + operation.getName() + ".groovy");
output.getParentFile().mkdirs();
try (Writer writer = new OutputStreamWriter(new FileOutputStream(output))) {
writer.append(content);
}
}
}
class JsonPaths {
private final String jsonPath;
JsonPaths(String jsonPath) {
this.jsonPath = jsonPath;
}
public String getJsonPath() {
return this.jsonPath;
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2012-2015 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.restdocs;
import java.util.Map;
import org.springframework.restdocs.snippet.Snippet;
/**
* Convenience class for setting up RestDocs to generate a {@link org.springframework.restdocs.snippet.Snippet}
* with Spring Cloud Contract DSL. Example usage:
*
* <pre>
* &#64;RunWith(SpringRunner.class)
* &#64;SpringBootTest
* &#64;AutoConfigureRestDocs(outputDir = "target/snippets")
* &#64;AutoConfigureMockMvc
* public class ContractRestDocsApplicationTests {
*
* &#64;Autowired
* private MockMvc mockMvc;
*
* &#64;Test
* public void contextLoads() throws Exception {
* this.mockMvc.perform(post("/foo")
* .accept(MediaType.APPLICATION_PDF)
* .accept(MediaType.APPLICATION_JSON)
* .contentType(MediaType.APPLICATION_JSON)
* .content("{\"foo\": 23 }"))
* .andExpect(status().isOk())
* .andExpect(content().string("bar"))
* // first WireMock
* .andDo(WireMockRestDocs.verify()
* .jsonPath("$[?(&#64;.foo >= 20)]")
* .contentType(MediaType.valueOf("application/json"))
* .stub("shouldGrantABeerIfOldEnough"))
* // then Contract DSL documentation
* .andDo(document("index", SpringCloudContractRestDocs.dslContract()));
* }
* </pre>
*
* which creates a file "target/snippets/contracts/index.groovy" and a
* standard documentation entitled `dsl-contract.adoc` containing that contract.
*
* @author Marcin Grzejszczak
* @since 1.0.4
*/
public class SpringCloudContractRestDocs {
private SpringCloudContractRestDocs() {
}
/**
* Returns a new {@code Snippet} that will document Spring Cloud Contract DSL for the API
* operation.
*
* @return the snippet that will document the Spring Cloud Contract DSL
*/
public static Snippet dslContract() {
return new ContractDslSnippet();
}
/**
* Returns a new {@code Snippet} that will document the Spring Cloud Contract DSL for the API
* operation. The given {@code attributes} will be available during snippet
* generation.
*
* @param attributes the attributes
* @return the snippet that will document the Spring Cloud Contract DSL
*/
public static Snippet dslContract(Map<String, Object> attributes) {
return new ContractDslSnippet(attributes);
}
}

View File

@@ -0,0 +1,42 @@
import org.springframework.cloud.contract.spec.Contract
Contract.make {
request {
method '{{request_method}}'
url '{{request_url}}'
{{#request_body_present}}
body('''
{{request_body}}
''')
{{/request_body_present}}
{{#request_headers_present}}
headers {
{{#request_headers}}
header('''{{key}}''', '''{{value}}''')
{{/request_headers}}
}
{{/request_headers_present}}
}
response {
status {{response_status}}
{{#response_body_present}}
body('''
{{response_body}}
''')
{{/response_body_present}}
{{#response_headers_present}}
headers {
{{#response_headers}}
header('''{{key}}''', '''{{value}}''')
{{/response_headers}}
}
{{/response_headers_present}}
{{#response_json_paths_present}}
testMatchers {
{{#response_json_paths}}
jsonPath('{{jsonPath}}', byType())
{{/response_json_paths}}
}
{{/response_json_paths_present}}
}
}

View File

@@ -0,0 +1,137 @@
package org.springframework.cloud.contract.wiremock.restdocs;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.BDDAssertions.then;
import static org.springframework.cloud.contract.wiremock.restdocs.SpringCloudContractRestDocs.dslContract;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Marcin Grzejszczak
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ContractDslSnippetTests.Config.class)
public class ContractDslSnippetTests {
private static final String OUTPUT = "target/generated-snippets";
@Rule
public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(OUTPUT);
MockMvc mockMvc;
@Autowired WebApplicationContext context;
@Before
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(documentationConfiguration(this.restDocumentation))
.build();
}
@Test
public void should_create_contract_template_and_doc() throws Exception {
//tag::contract_snippet[]
this.mockMvc.perform(post("/foo")
.accept(MediaType.APPLICATION_PDF)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"foo\": 23 }"))
.andExpect(status().isOk())
.andExpect(content().string("bar"))
// first WireMock
.andDo(WireMockRestDocs.verify()
.jsonPath("$[?(@.foo >= 20)]")
.contentType(MediaType.valueOf("application/json"))
.stub("shouldGrantABeerIfOldEnough"))
// then Contract DSL documentation
.andDo(document("index", SpringCloudContractRestDocs.dslContract()));
//end::contract_snippet[]
then(file("/contracts/index.groovy")).exists();
then(file("/index/dsl-contract.adoc")).exists();
String contract = readFromFile(file("/contracts/index.groovy"));
// try to parse the contract
Contract parsedContract = ContractVerifierDslConverter.convert(contract);
then(parsedContract.getRequest().getHeaders().getEntries()).isNotEmpty();
then(parsedContract.getRequest().getMethod().getClientValue()).isNotNull();
then(parsedContract.getRequest().getUrl().getClientValue()).isNotNull();
then(parsedContract.getRequest().getBody().getClientValue()).isNotNull();
then(parsedContract.getResponse().getStatus().getClientValue()).isNotNull();
then(parsedContract.getResponse().getHeaders().getEntries()).isNotEmpty();
then(parsedContract.getResponse().getBody().getClientValue()).isNotNull();
then(parsedContract.getResponse().getMatchers().hasMatchers()).isTrue();
}
@Test
public void should_create_contract_template_and_doc_without_body_and_headers() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders.get("/foo"))
.andExpect(status().isOk())
.andDo(document("empty", dslContract()));
then(file("/contracts/empty.groovy")).exists();
then(file("/empty/dsl-contract.adoc")).exists();
String contract = readFromFile(file("/contracts/empty.groovy"));
// try to parse the contract
Contract parsedContract = ContractVerifierDslConverter.convert(contract);
then(parsedContract.getRequest().getHeaders().getEntries()).isNotEmpty();
then(parsedContract.getRequest().getMethod().getClientValue()).isNotNull();
then(parsedContract.getRequest().getUrl().getClientValue()).isNotNull();
then(parsedContract.getRequest().getBody()).isNull();
then(parsedContract.getResponse().getStatus().getClientValue()).isNotNull();
then(parsedContract.getResponse().getHeaders()).isNull();
then(parsedContract.getResponse().getBody()).isNull();
then(parsedContract.getResponse().getMatchers()).isNull();
}
private File file(String name) throws URISyntaxException {
return new File(OUTPUT, name);
}
private String readFromFile(File f) throws IOException {
byte[] encoded = Files.readAllBytes(f.toPath());
return new String(encoded, StandardCharsets.UTF_8);
}
@Configuration
@EnableAutoConfiguration
@RestController
static class Config {
@PostMapping("/foo")
String foo() {
return "bar";
}
@GetMapping("/foo")
void getFoo() {
}
}
}