committed by
GitHub
parent
2c5b71222a
commit
a2fac97148
33
docs/pom.xml
33
docs/pom.xml
@@ -24,8 +24,24 @@
|
||||
<groovy.version>2.5.10</groovy.version>
|
||||
</properties>
|
||||
<build>
|
||||
<sourceDirectory>src/main/asciidoc</sourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>generate-sources</phase>
|
||||
<goals>
|
||||
<goal>add-source</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sources>
|
||||
<source>src/main/asciidoc</source>
|
||||
</sources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
@@ -75,12 +91,10 @@
|
||||
<groupId>com.fasterxml.jackson.module</groupId>
|
||||
<artifactId>jackson-module-jsonSchema</artifactId>
|
||||
<version>${jackson-module-jsonSchema.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.amqp</groupId>
|
||||
<artifactId>spring-amqp</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<profiles>
|
||||
@@ -115,6 +129,19 @@
|
||||
<executable>./build_adocs.sh</executable>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>generate-adoc-resources</id>
|
||||
<phase>process-classes</phase>
|
||||
<goals>
|
||||
<goal>java</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<mainClass>org.springframework.cloud.contract.docs.Main</mainClass>
|
||||
<arguments>
|
||||
<argument>${maven.multiModuleProjectDirectory}/docs</argument>
|
||||
</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
|
||||
@@ -439,3 +439,188 @@ Contract.make {
|
||||
|
||||
The generated document (formatted in Asciidoc in this case) contains a formatted
|
||||
contract. The location of this file would be `index/dsl-contract.adoc`.
|
||||
|
||||
[[features-graphql]]
|
||||
=== GraphQL
|
||||
|
||||
Since https://graphql.org/[GraphQL] is essentially HTTP you can write a contract for it by creating a standard HTTP contract with an additional `metadata` entry with key `verifier` and a mapping `tool=graphql`.
|
||||
|
||||
====
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Groovy
|
||||
----
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
|
||||
Contract.make {
|
||||
|
||||
request {
|
||||
method(POST())
|
||||
url("/graphql")
|
||||
headers {
|
||||
contentType("application/json")
|
||||
}
|
||||
body('''
|
||||
{
|
||||
"query":"query queryName($personName: String!) {\\n personToCheck(name: $personName) {\\n name\\n age\\n }\\n}\\n\\n\\n\\n",
|
||||
"variables":{"personName":"Old Enough"},
|
||||
"operationName":"queryName"
|
||||
}
|
||||
''')
|
||||
}
|
||||
|
||||
response {
|
||||
status(200)
|
||||
headers {
|
||||
contentType("application/json")
|
||||
}
|
||||
body('''\
|
||||
{
|
||||
"data": {
|
||||
"personToCheck": {
|
||||
"name": "Old Enough",
|
||||
"age": "40"
|
||||
}
|
||||
}
|
||||
}
|
||||
''')
|
||||
}
|
||||
metadata(verifier: [
|
||||
tool: "graphql"
|
||||
])
|
||||
}
|
||||
----
|
||||
|
||||
[source,yml,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.YAML
|
||||
----
|
||||
---
|
||||
request:
|
||||
method: "POST"
|
||||
url: "/graphql"
|
||||
headers:
|
||||
Content-Type: "application/json"
|
||||
body:
|
||||
query: "query queryName($personName: String!) { personToCheck(name: $personName)
|
||||
{ name age } }"
|
||||
variables:
|
||||
personName: "Old Enough"
|
||||
operationName: "queryName"
|
||||
matchers:
|
||||
headers:
|
||||
- key: "Content-Type"
|
||||
regex: "application/json.*"
|
||||
regexType: "as_string"
|
||||
response:
|
||||
status: 200
|
||||
headers:
|
||||
Content-Type: "application/json"
|
||||
body:
|
||||
data:
|
||||
personToCheck:
|
||||
name: "Old Enough"
|
||||
age: "40"
|
||||
matchers:
|
||||
headers:
|
||||
- key: "Content-Type"
|
||||
regex: "application/json.*"
|
||||
regexType: "as_string"
|
||||
name: "shouldRetrieveOldEnoughPerson"
|
||||
metadata:
|
||||
verifier:
|
||||
tool: "graphql"
|
||||
----
|
||||
====
|
||||
|
||||
Adding the metadata section will change the way the default, WireMock stub is built. It will now use the Spring Cloud Contract request matcher, so that e.g. the `query` part of the GraphQL request gets compared against the real request by ignoring whitespaces.
|
||||
|
||||
[[features-graphql-producer]]
|
||||
==== Producer Side Setup
|
||||
|
||||
On the producer side your configuration can look as follows.
|
||||
|
||||
====
|
||||
[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Maven
|
||||
----
|
||||
<plugin>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
|
||||
<version>${spring-cloud-contract.version}</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<testMode>EXPLICIT</testMode>
|
||||
<baseClassForTests>com.example.BaseClass</baseClassForTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
----
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.Gradle
|
||||
----
|
||||
contracts {
|
||||
testMode = "EXPLICIT"
|
||||
baseClassForTests = "com.example.BaseClass"
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
The base class would set up the applicatoin running on a random port.
|
||||
|
||||
====
|
||||
[source,java,indent=0,subs="verbatim,attributes"]
|
||||
.Base Class
|
||||
----
|
||||
@SpringBootTest(classes = ProducerApplication.class,
|
||||
properties = "graphql.servlet.websocket.enabled=false",
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
public abstract class BaseClass {
|
||||
|
||||
@LocalServerPort int port;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
RestAssured.baseURI = "http://localhost:" + port;
|
||||
}
|
||||
}
|
||||
|
||||
----
|
||||
====
|
||||
|
||||
[[features-graphql-consumer]]
|
||||
==== Consumer Side Setup
|
||||
|
||||
Example of a consumer side test of the GraphQL API.
|
||||
|
||||
====
|
||||
[source,java,indent=0,subs="verbatim,attributes"]
|
||||
.Consumer Side Test
|
||||
----
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
|
||||
public class BeerControllerGraphQLTest {
|
||||
|
||||
@RegisterExtension
|
||||
static StubRunnerExtension rule = new StubRunnerExtension()
|
||||
.downloadStub("com.example","beer-api-producer-graphql")
|
||||
.stubsMode(StubRunnerProperties.StubsMode.LOCAL);
|
||||
|
||||
private static final String REQUEST_BODY = "{\n"
|
||||
+ "\"query\":\"query queryName($personName: String!) {\\n personToCheck(name: $personName) {\\n name\\n age\\n }\\n}\","
|
||||
+ "\"variables\":{\"personName\":\"Old Enough\"},\n"
|
||||
+ "\"operationName\":\"queryName\"\n"
|
||||
+ "}";
|
||||
|
||||
@Test
|
||||
public void should_send_a_graphql_request() {
|
||||
ResponseEntity<String> responseEntity = new RestTemplate()
|
||||
.exchange(RequestEntity
|
||||
.post(URI.create("http://localhost:" + rule.findStubUrl("beer-api-producer-graphql").getPort() + "/graphql"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(REQUEST_BODY), String.class);
|
||||
|
||||
BDDAssertions.then(responseEntity.getStatusCodeValue()).isEqualTo(200);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
----
|
||||
====
|
||||
@@ -81,7 +81,7 @@ You can pick from the following options of acquiring stubs:
|
||||
- Classpath-scanning solution that searches the classpath with a pattern to retrieve stubs
|
||||
- Writing your own implementation of the `org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder` for full customization
|
||||
|
||||
The latter example is described in the <<advanced.adoc#customization-custom-stub-runner, Custom Stub Runner>> section.
|
||||
The latter example is described in the <<advanced.html#customization-custom-stub-runner, Custom Stub Runner>> section.
|
||||
|
||||
[[features-stub-runner-downloading-stub]]
|
||||
===== Downloading Stubs
|
||||
|
||||
@@ -111,18 +111,18 @@ link:docker-project.html[Docker]
|
||||
Finally, we have a few topics for more advanced users:
|
||||
|
||||
* *Customizing the DSL:*
|
||||
<<advanced.adoc#contract-dsl-customization, DSL Customization>> |
|
||||
<<advanced.adoc#contract-dsl-extending-common-jar, Common JAR>> |
|
||||
<<advanced.adoc#contract-dsl-test-dep, Test Dependency>> |
|
||||
<<advanced.adoc#contract-dsl-plugin-dep, Plugin Dependency>> |
|
||||
<<advanced.adoc#contract-dsl-referencing, Referencing the DSL>>
|
||||
<<advanced.html#contract-dsl-customization, DSL Customization>> |
|
||||
<<advanced.html#contract-dsl-extending-common-jar, Common JAR>> |
|
||||
<<advanced.html#contract-dsl-test-dep, Test Dependency>> |
|
||||
<<advanced.html#contract-dsl-plugin-dep, Plugin Dependency>> |
|
||||
<<advanced.html#contract-dsl-referencing, Referencing the DSL>>
|
||||
* *Customizing WireMock:*
|
||||
<<advanced.adoc#customization-wiremock-extension, Extensions>> |
|
||||
<<advanced.adoc#customization-wiremock-configuration, Configuration>>
|
||||
<<advanced.html#customization-wiremock-extension, Extensions>> |
|
||||
<<advanced.html#customization-wiremock-configuration, Configuration>>
|
||||
* *Customizing {project-full-name}:*
|
||||
<<advanced.adoc#contract-dsl-pluggable-architecture, Pluggable Architecture>> |
|
||||
<<advanced.adoc#contract-dsl-custom-contract-converter, Contract Converter>> |
|
||||
<<advanced.adoc#contract-dsl-custom-test-generator, Test Generator>> |
|
||||
<<advanced.adoc#contract-dsl-custom-stub-generator, Stub Generator>> |
|
||||
<<advanced.adoc#contract-dsl-custom-stub-runner, Stub Runner>> |
|
||||
<<advanced.adoc#contract-dsl-custom-stub-downloader, Stub Downloader>>
|
||||
<<advanced.html#contract-dsl-pluggable-architecture, Pluggable Architecture>> |
|
||||
<<advanced.html#contract-dsl-custom-contract-converter, Contract Converter>> |
|
||||
<<advanced.html#contract-dsl-custom-test-generator, Test Generator>> |
|
||||
<<advanced.html#contract-dsl-custom-stub-generator, Stub Generator>> |
|
||||
<<advanced.html#contract-dsl-custom-stub-runner, Stub Runner>> |
|
||||
<<advanced.html#contract-dsl-custom-stub-downloader, Stub Downloader>>
|
||||
|
||||
@@ -37,4 +37,4 @@ If you want to learn more about any of the classes discussed in this section, yo
|
||||
|
||||
If you are comfortable with {project-full-name}'s core features, you can continue on and read
|
||||
about
|
||||
<<advanced.adoc, {project-full-name}'s advanced features>>.
|
||||
<<advanced.html, {project-full-name}'s advanced features>>.
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright 2012-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.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.docs;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
|
||||
import com.fasterxml.jackson.module.jsonSchema.JsonSchema;
|
||||
import com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.util.SpringCloudContractMetadata;
|
||||
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
|
||||
import org.springframework.core.type.filter.AssignableTypeFilter;
|
||||
import org.springframework.core.type.filter.TypeFilter;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
public class Main {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Main.class);
|
||||
|
||||
private final File rootPath;
|
||||
|
||||
public Main(File rootPath) {
|
||||
this.rootPath = rootPath;
|
||||
}
|
||||
|
||||
public static void main(String... args) throws Exception {
|
||||
File rootPath = new File(args[0]);
|
||||
Main main = new Main(rootPath);
|
||||
main.produceJsonSchemaOfAYamlModel();
|
||||
main.produceAdocWithAllOfMetadataClasses();
|
||||
}
|
||||
|
||||
void produceJsonSchemaOfAYamlModel() throws IOException {
|
||||
log.info("Generating schema...");
|
||||
String schemaString = generateJsonSchemaForClass(YamlContract.class);
|
||||
File schemaFile = new File(this.rootPath, "target/contract_schema.json");
|
||||
Files.write(schemaFile.toPath(), schemaString.getBytes());
|
||||
log.info("Generated schema!");
|
||||
}
|
||||
|
||||
private String generateJsonSchemaForClass(Class clazz)
|
||||
throws JsonProcessingException {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
JsonSchemaGenerator schemaGen = new JsonSchemaGenerator(mapper);
|
||||
JsonSchema schema = schemaGen.generateSchema(clazz);
|
||||
return mapper.writeValueAsString(schema);
|
||||
}
|
||||
|
||||
void produceAdocWithAllOfMetadataClasses() throws Exception {
|
||||
log.info("Produce adoc with all metadata...");
|
||||
List<Class> metadata = metadataClasses();
|
||||
File doc = new File(this.rootPath, "target/metadata.adoc");
|
||||
StringBuilder sb = adocWithMetadata(metadata);
|
||||
Files.write(doc.toPath(), sb.toString().getBytes());
|
||||
log.info("Produced adoc with all metadata!");
|
||||
}
|
||||
|
||||
private StringBuilder adocWithMetadata(List<Class> metadata) throws Exception {
|
||||
YAMLMapper mapper = new YAMLMapper();
|
||||
mapper.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
mapper.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Class metadatum : metadata) {
|
||||
SpringCloudContractMetadata newInstance = (SpringCloudContractMetadata) metadatum
|
||||
.newInstance();
|
||||
String description = newInstance.description();
|
||||
String key = newInstance.key();
|
||||
List<Class> additionalClasses = classesToLookAt(metadatum, newInstance);
|
||||
// @formatter:off
|
||||
sb
|
||||
.append("[[metadata-").append(key).append("]]\n")
|
||||
.append("##### Metadata `").append(key).append("`\n\n")
|
||||
.append("* key: `").append(key).append("`").append("\n")
|
||||
.append("* description:\n\n").append(description).append("\n\n")
|
||||
.append("Example:\n\n")
|
||||
.append("```yaml\n").append(mapper.writeValueAsString(newInstance)).append("\n```\n\n")
|
||||
// To make the schema collapsable
|
||||
.append("+++ <details><summary> +++\nClick here to expand the JSON schema:\n+++ </summary><div> +++\n")
|
||||
.append("```json\n").append(generateJsonSchemaForClass(metadatum)).append("\n```\n")
|
||||
.append("+++ </div></details> +++\n\n")
|
||||
.append("If you are interested in learning more about the types and its properties, check out the following classes:\n\n")
|
||||
.append(additionalClasses.stream().map(aClass -> "* `" + aClass.getName() + "`").collect(Collectors.joining("\n")))
|
||||
.append("\n\n");
|
||||
// @formatter:on
|
||||
}
|
||||
return sb;
|
||||
}
|
||||
|
||||
private List<Class> classesToLookAt(Class metadatum,
|
||||
SpringCloudContractMetadata newInstance) {
|
||||
List<Class> additionalClasses = new ArrayList<>();
|
||||
additionalClasses.add(metadatum);
|
||||
additionalClasses.addAll(newInstance.additionalClassesToLookAt());
|
||||
return additionalClasses;
|
||||
}
|
||||
|
||||
private List<Class> metadataClasses() throws ClassNotFoundException {
|
||||
BeanDefinitionRegistry bdr = new SimpleBeanDefinitionRegistry();
|
||||
ClassPathBeanDefinitionScanner s = new ClassPathBeanDefinitionScanner(bdr, false);
|
||||
TypeFilter tf = new AssignableTypeFilter(SpringCloudContractMetadata.class);
|
||||
s.addIncludeFilter(tf);
|
||||
String basePackage = "org.springframework.cloud.contract";
|
||||
s.scan(basePackage);
|
||||
String[] beans = bdr.getBeanDefinitionNames();
|
||||
List<Class> metadata = new ArrayList<>();
|
||||
for (String bean : beans) {
|
||||
BeanDefinition beanDefinition = bdr.getBeanDefinition(bean);
|
||||
String beanClassName = beanDefinition.getBeanClassName();
|
||||
if (beanClassName != null && !beanClassName.contains(basePackage)) {
|
||||
continue;
|
||||
}
|
||||
metadata.add(Class.forName(beanClassName));
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,36 +19,14 @@ package org.springframework.cloud.contract.docs;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
|
||||
import com.fasterxml.jackson.module.jsonSchema.JsonSchema;
|
||||
import com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator;
|
||||
import org.assertj.core.api.BDDAssertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry;
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContractConverter;
|
||||
import org.springframework.cloud.contract.verifier.util.SpringCloudContractMetadata;
|
||||
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
|
||||
import org.springframework.core.type.filter.AssignableTypeFilter;
|
||||
import org.springframework.core.type.filter.TypeFilter;
|
||||
|
||||
/**
|
||||
* This test generates additional resources in the `target` folder that are then
|
||||
* referenced by the documentation.
|
||||
*/
|
||||
class AdditionalResourcesGenerationTests {
|
||||
|
||||
// @formatter:off
|
||||
@@ -113,22 +91,6 @@ class AdditionalResourcesGenerationTests {
|
||||
+ " predefined:\n";
|
||||
// @formatter:on
|
||||
|
||||
@Test
|
||||
void should_produce_a_json_schema_of_a_yaml_model() throws IOException {
|
||||
String schemaString = generateJsonSchemaForClass(YamlContract.class);
|
||||
File schemaFile = new File("target/contract_schema.json");
|
||||
Files.write(schemaFile.toPath(), schemaString.getBytes());
|
||||
}
|
||||
|
||||
private String generateJsonSchemaForClass(Class clazz)
|
||||
throws JsonProcessingException {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
JsonSchemaGenerator schemaGen = new JsonSchemaGenerator(mapper);
|
||||
JsonSchema schema = schemaGen.generateSchema(clazz);
|
||||
return mapper.writeValueAsString(schema);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_convert_yaml_to_contract() throws IOException {
|
||||
File ymlFile = new File("target/contract.yml");
|
||||
@@ -139,73 +101,4 @@ class AdditionalResourcesGenerationTests {
|
||||
BDDAssertions.then(contracts).isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_produce_an_adoc_with_all_of_metadata_classes() throws Exception {
|
||||
List<Class> metadata = metadataClasses();
|
||||
File doc = new File("target/metadata.adoc");
|
||||
|
||||
StringBuilder sb = adocWithMetadata(metadata);
|
||||
|
||||
Files.write(doc.toPath(), sb.toString().getBytes());
|
||||
}
|
||||
|
||||
private StringBuilder adocWithMetadata(List<Class> metadata) throws Exception {
|
||||
YAMLMapper mapper = new YAMLMapper();
|
||||
mapper.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
mapper.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Class metadatum : metadata) {
|
||||
SpringCloudContractMetadata newInstance = (SpringCloudContractMetadata) metadatum
|
||||
.newInstance();
|
||||
String description = newInstance.description();
|
||||
String key = newInstance.key();
|
||||
List<Class> additionalClasses = classesToLookAt(metadatum, newInstance);
|
||||
// @formatter:off
|
||||
sb
|
||||
.append("[[metadata-").append(key).append("]]\n")
|
||||
.append("##### Metadata `").append(key).append("`\n\n")
|
||||
.append("* key: `").append(key).append("`").append("\n")
|
||||
.append("* description:\n\n").append(description).append("\n\n")
|
||||
.append("Example:\n\n")
|
||||
.append("```yaml\n").append(mapper.writeValueAsString(newInstance)).append("\n```\n\n")
|
||||
// To make the schema collapsable
|
||||
.append("+++ <details><summary> +++\nClick here to expand the JSON schema:\n+++ </summary><div> +++\n")
|
||||
.append("```json\n").append(generateJsonSchemaForClass(metadatum)).append("\n```\n")
|
||||
.append("+++ </div></details> +++\n\n")
|
||||
.append("If you are interested in learning more about the types and its properties, check out the following classes:\n\n")
|
||||
.append(additionalClasses.stream().map(aClass -> "* `" + aClass.getName() + "`").collect(Collectors.joining("\n")))
|
||||
.append("\n\n");
|
||||
// @formatter:on
|
||||
}
|
||||
return sb;
|
||||
}
|
||||
|
||||
private List<Class> classesToLookAt(Class metadatum,
|
||||
SpringCloudContractMetadata newInstance) {
|
||||
List<Class> additionalClasses = new ArrayList<>();
|
||||
additionalClasses.add(metadatum);
|
||||
additionalClasses.addAll(newInstance.additionalClassesToLookAt());
|
||||
return additionalClasses;
|
||||
}
|
||||
|
||||
private List<Class> metadataClasses() throws ClassNotFoundException {
|
||||
BeanDefinitionRegistry bdr = new SimpleBeanDefinitionRegistry();
|
||||
ClassPathBeanDefinitionScanner s = new ClassPathBeanDefinitionScanner(bdr, false);
|
||||
TypeFilter tf = new AssignableTypeFilter(SpringCloudContractMetadata.class);
|
||||
s.addIncludeFilter(tf);
|
||||
String basePackage = "org.springframework.cloud.contract";
|
||||
s.scan(basePackage);
|
||||
String[] beans = bdr.getBeanDefinitionNames();
|
||||
List<Class> metadata = new ArrayList<>();
|
||||
for (String bean : beans) {
|
||||
BeanDefinition beanDefinition = bdr.getBeanDefinition(bean);
|
||||
String beanClassName = beanDefinition.getBeanClassName();
|
||||
if (beanClassName != null && !beanClassName.contains(basePackage)) {
|
||||
continue;
|
||||
}
|
||||
metadata.add(Class.forName(beanClassName));
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
1
pom.xml
1
pom.xml
@@ -45,6 +45,7 @@
|
||||
<jgit.version>5.5.1.201910021850-r</jgit.version>
|
||||
<javax-inject.version>1</javax-inject.version>
|
||||
<mockito.version>3.4.6</mockito.version>
|
||||
<json-unit-assertj.version>2.19.0</json-unit-assertj.version>
|
||||
|
||||
<junit-platform.version>1.3.2</junit-platform.version>
|
||||
<junit-vintage.version>5.6.2</junit-vintage.version>
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -46,6 +47,7 @@ import org.springframework.cloud.contract.stubrunner.HttpServerStubConfigurer;
|
||||
import org.springframework.cloud.contract.verifier.builder.handlebars.HandlebarsEscapeHelper;
|
||||
import org.springframework.cloud.contract.verifier.builder.handlebars.HandlebarsJsonPathHelper;
|
||||
import org.springframework.cloud.contract.verifier.dsl.wiremock.DefaultResponseTransformer;
|
||||
import org.springframework.cloud.contract.verifier.dsl.wiremock.SpringCloudContractRequestMatcher;
|
||||
import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockExtensions;
|
||||
import org.springframework.cloud.contract.wiremock.WireMockSpring;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
@@ -92,7 +94,9 @@ public class WireMockHttpServerStub implements HttpServerStub {
|
||||
}
|
||||
}
|
||||
else {
|
||||
extensions.add(new DefaultResponseTransformer(false, helpers()));
|
||||
extensions.addAll(
|
||||
Arrays.asList(new DefaultResponseTransformer(false, helpers()),
|
||||
new SpringCloudContractRequestMatcher()));
|
||||
}
|
||||
return extensions.toArray(new Extension[extensions.size()]);
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ class DslToWireMockClientConverterSpec extends Specification {
|
||||
ContractVerifierDslConverter.convertAsCollection(new File("/"), file))).values().first()
|
||||
then:
|
||||
JSONAssert.assertEquals('''
|
||||
{"request":{"url":"/multipart","method":"POST","headers":{"Content-Type":{"matches":"multipart/form-data.*"}},"bodyPatterns":[{"matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"file\\"; filename=\\".+\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Transfer-Encoding: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n.+\\r\\n--\\\\1.*"}]},"response":{"status":200,"body":"hello","transformers":["response-template"]}}
|
||||
{"request":{"url":"/multipart","method":"POST","headers":{"Content-Type":{"matches":"multipart/form-data.*"}},"bodyPatterns":[{"matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"file\\"; filename=\\".+\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Transfer-Encoding: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n.+\\r\\n--\\\\1.*"}]},"response":{"status":200,"body":"hello","transformers":["response-template", "spring-cloud-contract" ]}}
|
||||
''', json, false)
|
||||
and:
|
||||
StubMapping mapping = stubMappingIsValidWireMockStub(json)
|
||||
@@ -752,7 +752,7 @@ class DslToWireMockClientConverterSpec extends Specification {
|
||||
"headers" : {
|
||||
"Content-Type" : "application/json"
|
||||
},
|
||||
"transformers" : [ "response-template" ]
|
||||
"transformers" : [ "response-template", "spring-cloud-contract" ]
|
||||
}
|
||||
}
|
||||
'''
|
||||
@@ -992,7 +992,7 @@ class DslToWireMockClientConverterSpec extends Specification {
|
||||
"CorrelationID" : "11111111-1111-1111-1111-111111111111",
|
||||
"Content-Type" : "application/json;charset=UTF-8"
|
||||
},
|
||||
"transformers" : [ "response-template" ]
|
||||
"transformers" : [ "response-template", "spring-cloud-contract" ]
|
||||
},
|
||||
"priority" : 1
|
||||
}
|
||||
|
||||
@@ -189,6 +189,12 @@
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.javacrumbs.json-unit</groupId>
|
||||
<artifactId>json-unit-assertj</artifactId>
|
||||
<version>${json-unit-assertj.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.cloud.contract.verifier.dsl.wiremock
|
||||
import java.util.regex.Pattern
|
||||
|
||||
import com.github.tomakehurst.wiremock.client.WireMock
|
||||
import com.github.tomakehurst.wiremock.extension.Parameters
|
||||
import com.github.tomakehurst.wiremock.http.RequestMethod
|
||||
import com.github.tomakehurst.wiremock.matching.ContentPattern
|
||||
import com.github.tomakehurst.wiremock.matching.RequestPattern
|
||||
@@ -45,6 +46,9 @@ import org.springframework.cloud.contract.spec.internal.QueryParameters
|
||||
import org.springframework.cloud.contract.spec.internal.RegexPatterns
|
||||
import org.springframework.cloud.contract.spec.internal.RegexProperty
|
||||
import org.springframework.cloud.contract.spec.internal.Request
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContractConverter
|
||||
import org.springframework.cloud.contract.verifier.dsl.ContractVerifierMetadata
|
||||
import org.springframework.cloud.contract.verifier.file.SingleContractMetadata
|
||||
import org.springframework.cloud.contract.verifier.util.ContentType
|
||||
import org.springframework.cloud.contract.verifier.util.ContentUtils
|
||||
@@ -52,6 +56,7 @@ import org.springframework.cloud.contract.verifier.util.JsonPaths
|
||||
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter
|
||||
import org.springframework.cloud.contract.verifier.util.MapConverter
|
||||
import org.springframework.cloud.contract.verifier.util.xml.XmlToXPathsConverter
|
||||
import org.springframework.util.StringUtils
|
||||
|
||||
import static org.springframework.cloud.contract.spec.internal.MatchingStrategy.Type.BINARY_EQUAL_TO
|
||||
import static org.springframework.cloud.contract.spec.internal.MatchingType.COMMAND
|
||||
@@ -105,6 +110,28 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy {
|
||||
return requestPatternBuilder.build()
|
||||
}
|
||||
|
||||
private void appendBody(RequestPatternBuilder requestPatternBuilder) {
|
||||
if (contract.metadata.containsKey(ContractVerifierMetadata.METADATA_KEY)) {
|
||||
ContractVerifierMetadata metadata = ContractVerifierMetadata.fromMetadata(contract.getMetadata())
|
||||
appendSpringCloudContractMatcher(metadata, requestPatternBuilder)
|
||||
if (StringUtils.isEmpty(metadata.getTool())) {
|
||||
doAppendBody(requestPatternBuilder)
|
||||
}
|
||||
}
|
||||
else {
|
||||
doAppendBody(requestPatternBuilder)
|
||||
}
|
||||
}
|
||||
|
||||
private void appendSpringCloudContractMatcher(ContractVerifierMetadata metadata, RequestPatternBuilder requestPatternBuilder) {
|
||||
Parameters parameters = Parameters.one("tool", metadata.getTool() ?: "unknown");
|
||||
YamlContractConverter converter = new YamlContractConverter();
|
||||
List<YamlContract> contracts = converter.convertTo(Collections.singleton(contract));
|
||||
Map<String, byte[]> store = converter.store(contracts);
|
||||
parameters.put("contract", new String(store.entrySet().iterator().next().value))
|
||||
requestPatternBuilder.andMatching(SpringCloudContractRequestMatcher.NAME, parameters)
|
||||
}
|
||||
|
||||
private RequestPatternBuilder appendMethodAndUrl() {
|
||||
if (!request.method) {
|
||||
return null
|
||||
@@ -115,7 +142,7 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy {
|
||||
return RequestPatternBuilder.newRequestPattern(requestMethod, urlPattern)
|
||||
}
|
||||
|
||||
private void appendBody(RequestPatternBuilder requestPattern) {
|
||||
private void doAppendBody(RequestPatternBuilder requestPattern) {
|
||||
if (!request.body) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2012-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.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.dsl;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.util.MetadataUtil;
|
||||
import org.springframework.cloud.contract.verifier.util.SpringCloudContractMetadata;
|
||||
import org.springframework.lang.NonNull;
|
||||
|
||||
/**
|
||||
* Metadata representation of the Contract Verifier.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class ContractVerifierMetadata implements SpringCloudContractMetadata {
|
||||
|
||||
/**
|
||||
* Metadata entry in the contract.
|
||||
*/
|
||||
public static final String METADATA_KEY = "verifier";
|
||||
|
||||
public ContractVerifierMetadata(String tool) {
|
||||
this.tool = tool;
|
||||
}
|
||||
|
||||
public ContractVerifierMetadata() {
|
||||
}
|
||||
|
||||
private String tool;
|
||||
|
||||
public String getTool() {
|
||||
return this.tool;
|
||||
}
|
||||
|
||||
public void setTool(String tool) {
|
||||
this.tool = tool;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static ContractVerifierMetadata fromMetadata(Map<String, Object> metadata) {
|
||||
return MetadataUtil.fromMetadata(metadata, METADATA_KEY,
|
||||
new ContractVerifierMetadata());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String key() {
|
||||
return METADATA_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Metadata entries used by the framework";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.dsl.wiremock;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.github.tomakehurst.wiremock.extension.Parameters;
|
||||
import com.github.tomakehurst.wiremock.http.Request;
|
||||
import com.github.tomakehurst.wiremock.matching.MatchResult;
|
||||
import com.github.tomakehurst.wiremock.matching.RequestMatcherExtension;
|
||||
import net.javacrumbs.jsonunit.assertj.JsonAssertions;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.codehaus.plexus.util.StringUtils;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContractConverter;
|
||||
|
||||
/**
|
||||
* Provides custom matching for WireMock's stub requests.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class SpringCloudContractRequestMatcher extends RequestMatcherExtension {
|
||||
|
||||
private static final List<String> SUPPORTED_TOOLS = Arrays
|
||||
.asList(GraphQlMatcher.NAME);
|
||||
|
||||
/**
|
||||
* Name of the transformer inside the stub.
|
||||
*/
|
||||
public static final String NAME = "spring-cloud-contract";
|
||||
|
||||
private static final Log log = LogFactory
|
||||
.getLog(SpringCloudContractRequestMatcher.class);
|
||||
|
||||
@Override
|
||||
public MatchResult match(Request request, Parameters parameters) {
|
||||
if (!parameters.containsKey("contract") || !parameters.containsKey("tool")) {
|
||||
return MatchResult.noMatch();
|
||||
}
|
||||
String tool = parameters.getString("tool");
|
||||
if (!SUPPORTED_TOOLS.contains(tool)) {
|
||||
if (log.isWarnEnabled()) {
|
||||
log.warn("The tool [" + tool + "] is not supported");
|
||||
}
|
||||
return MatchResult.noMatch();
|
||||
}
|
||||
String string = parameters.getString("contract");
|
||||
List<YamlContract> contracts;
|
||||
try {
|
||||
contracts = YamlContractConverter.INSTANCE.read(string.getBytes());
|
||||
}
|
||||
catch (Exception e) {
|
||||
if (log.isWarnEnabled()) {
|
||||
log.warn("An exception occurred while trying to parse the contract", e);
|
||||
}
|
||||
return MatchResult.noMatch();
|
||||
}
|
||||
return new RequestMatcherFactory(matchers()).pick(tool).match(contracts, request,
|
||||
parameters);
|
||||
}
|
||||
|
||||
List<RequestMatcher> matchers() {
|
||||
return Arrays.asList(new GraphQlMatcher());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class RequestMatcherFactory {
|
||||
|
||||
private final List<RequestMatcher> matchers;
|
||||
|
||||
RequestMatcherFactory(List<RequestMatcher> matchers) {
|
||||
this.matchers = matchers;
|
||||
}
|
||||
|
||||
RequestMatcher pick(String tool) {
|
||||
return this.matchers.stream().filter(m -> m.isApplicable(tool)).findFirst()
|
||||
.orElse(new NotMatchingRequestMatcher());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface RequestMatcher {
|
||||
|
||||
MatchResult match(List<YamlContract> contracts, Request request,
|
||||
Parameters parameters);
|
||||
|
||||
default boolean assertThat(Runnable runnable) {
|
||||
try {
|
||||
runnable.run();
|
||||
return true;
|
||||
}
|
||||
catch (Exception | AssertionError er) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
default boolean isApplicable(String tool) {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class NotMatchingRequestMatcher implements RequestMatcher {
|
||||
|
||||
@Override
|
||||
public MatchResult match(List<YamlContract> contracts, Request request,
|
||||
Parameters parameters) {
|
||||
return MatchResult.noMatch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(String tool) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class GraphQlMatcher implements RequestMatcher {
|
||||
|
||||
static final String NAME = "graphql";
|
||||
|
||||
private static final Log log = LogFactory.getLog(GraphQlMatcher.class);
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public MatchResult match(List<YamlContract> contracts, Request request,
|
||||
Parameters parameters) {
|
||||
YamlContract contract = contracts.get(0);
|
||||
// TODO: What if the body is in files?
|
||||
Map body = (Map) contract.request.body;
|
||||
try {
|
||||
Map jsonBodyFromContract = body;
|
||||
Map jsonBodyFromRequest = this.objectMapper.readerForMapOf(Object.class)
|
||||
.readValue(request.getBody());
|
||||
String query = (String) jsonBodyFromContract.get("query");
|
||||
String queryFromRequest = (String) jsonBodyFromRequest.get("query");
|
||||
Map variables = (Map) jsonBodyFromContract.get("variables");
|
||||
Map variablesFromRequest = (Map) jsonBodyFromRequest.get("variables");
|
||||
String operationName = (String) jsonBodyFromContract.get("operationName");
|
||||
String operationNameFromRequest = (String) jsonBodyFromRequest
|
||||
.get("operationName");
|
||||
boolean queryMatches = assertThat(() -> Assertions.assertThat(query)
|
||||
.isEqualToIgnoringWhitespace(queryFromRequest));
|
||||
boolean variablesMatch = assertThat(() -> JsonAssertions
|
||||
.assertThatJson(variables).isEqualTo(variablesFromRequest));
|
||||
boolean operationMatches = StringUtils.equals(operationName,
|
||||
operationNameFromRequest);
|
||||
return MatchResult.of(queryMatches && variablesMatch && operationMatches);
|
||||
}
|
||||
catch (Exception e) {
|
||||
if (log.isWarnEnabled()) {
|
||||
log.warn(
|
||||
"An exception occurred while trying to parse the graphql entries",
|
||||
e);
|
||||
}
|
||||
return MatchResult.noMatch();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(String tool) {
|
||||
return NAME.equals(tool);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -84,7 +84,8 @@ class WireMockResponseStubStrategy extends BaseWireMockStubStrategy {
|
||||
.flatMap(Collection::stream).map(Extension::getName)
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
return new String[] { new DefaultResponseTransformer().getName() };
|
||||
return new String[] { new DefaultResponseTransformer().getName(),
|
||||
SpringCloudContractRequestMatcher.NAME };
|
||||
}
|
||||
|
||||
private void appendHeaders(ResponseDefinitionBuilder builder) {
|
||||
|
||||
@@ -64,7 +64,7 @@ public class ContractVerifierMessageMetadata implements SpringCloudContractMetad
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Internal metadata entries used by the framework";
|
||||
return "Internal metadata entries used by the framework, related to messaging";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2020-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.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.dsl;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
|
||||
import org.assertj.core.api.BDDAssertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ContractVerifierMetadataTests {
|
||||
|
||||
YAMLMapper mapper = new YAMLMapper();
|
||||
|
||||
@Test
|
||||
void should_parse_the_metadata_entry() throws JsonProcessingException {
|
||||
// @formatter:off
|
||||
String yamlEntry = "verifier:\n"
|
||||
+ " tool: graphql";
|
||||
// @formatter:on
|
||||
|
||||
ContractVerifierMetadata metadata = ContractVerifierMetadata.fromMetadata(
|
||||
this.mapper.readerForMapOf(Object.class).readValue(yamlEntry));
|
||||
|
||||
String serialized = this.mapper.writer().forType(ContractVerifierMetadata.class)
|
||||
.writeValueAsString(metadata);
|
||||
BDDAssertions.then(serialized).isEqualToNormalizingPunctuationAndWhitespace(
|
||||
yamlEntry.replace("verifier:\n", ""));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright 2020-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.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.dsl.wiremock;
|
||||
|
||||
import com.github.tomakehurst.wiremock.http.Request;
|
||||
import com.github.tomakehurst.wiremock.matching.MatchResult;
|
||||
import org.assertj.core.api.BDDAssertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.BDDMockito;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContractConverter;
|
||||
|
||||
class GraphQLRequestMatcherTests {
|
||||
|
||||
// @formatter:off
|
||||
private static final String YAML_WITH_INVALID_VARIABLES = "---\n"
|
||||
+ "request:\n"
|
||||
+ " method: \"POST\"\n"
|
||||
+ " url: \"/graphql\"\n"
|
||||
+ " headers:\n"
|
||||
+ " Content-Type: \"application/json\"\n"
|
||||
+ " body:\n"
|
||||
+ " query: \"query queryName($personName: String!) {\\n personToCheck(name: $personName)\\\n"
|
||||
+ " \\ {\\n name\\n age\\n }\\n}\\n\\n\\n\\n\"\n"
|
||||
+ " variables: This should actually be a map not a string\n"
|
||||
+ " operationName: \"queryName\"\n"
|
||||
+ " matchers:\n"
|
||||
+ " headers:\n"
|
||||
+ " - key: \"Content-Type\"\n"
|
||||
+ " regex: \"application/json.*\"\n"
|
||||
+ " regexType: \"as_string\"\n"
|
||||
+ "response:\n"
|
||||
+ " status: 200\n"
|
||||
+ " headers:\n"
|
||||
+ " Content-Type: \"application/json\"\n"
|
||||
+ " body:\n"
|
||||
+ " data:\n"
|
||||
+ " personToCheck:\n"
|
||||
+ " name: \"Old Enough\"\n"
|
||||
+ " age: \"40\"\n"
|
||||
+ " matchers:\n"
|
||||
+ " headers:\n"
|
||||
+ " - key: \"Content-Type\"\n"
|
||||
+ " regex: \"application/json.*\"\n"
|
||||
+ " regexType: \"as_string\"\n"
|
||||
+ "name: \"shouldRetrieveOldEnoughPerson\"\n"
|
||||
+ "metadata:\n"
|
||||
+ " verifier:\n"
|
||||
+ " tool: \"graphql\"\n";
|
||||
// @formatter:on
|
||||
|
||||
@Test
|
||||
void should_not_match_when_exception_occurs_while_trying_to_read_missing_request_body() {
|
||||
GraphQlMatcher matcher = new GraphQlMatcher();
|
||||
|
||||
MatchResult result = matcher.match(
|
||||
YamlContractConverter.INSTANCE
|
||||
.read(YAML_WITH_INVALID_VARIABLES.getBytes()),
|
||||
BDDMockito.mock(Request.class), null);
|
||||
|
||||
BDDAssertions.then(result.isExactMatch()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_not_match_when_exception_occurs_while_trying_to_parse_graphql_entries() {
|
||||
GraphQlMatcher matcher = new GraphQlMatcher();
|
||||
|
||||
MatchResult result = matcher.match(YamlContractConverter.INSTANCE
|
||||
.read(YAML_WITH_INVALID_VARIABLES.getBytes()), request(), null);
|
||||
|
||||
BDDAssertions.then(result.isExactMatch()).isFalse();
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
private static final String PROPER_YAML = "---\n"
|
||||
+ "request:\n"
|
||||
+ " method: \"POST\"\n"
|
||||
+ " url: \"/graphql\"\n"
|
||||
+ " headers:\n"
|
||||
+ " Content-Type: \"application/json\"\n"
|
||||
+ " body:\n"
|
||||
+ " query: \"query queryName($personName: String!) { personToCheck(name: $personName)"
|
||||
+ " { name age } }\"\n"
|
||||
+ " variables:\n"
|
||||
+ " personName: \"Old Enough\"\n"
|
||||
+ " operationName: \"queryName\"\n"
|
||||
+ " matchers:\n"
|
||||
+ " headers:\n"
|
||||
+ " - key: \"Content-Type\"\n"
|
||||
+ " regex: \"application/json.*\"\n"
|
||||
+ " regexType: \"as_string\"\n"
|
||||
+ "response:\n"
|
||||
+ " status: 200\n"
|
||||
+ " headers:\n"
|
||||
+ " Content-Type: \"application/json\"\n"
|
||||
+ " body:\n"
|
||||
+ " data:\n"
|
||||
+ " personToCheck:\n"
|
||||
+ " name: \"Old Enough\"\n"
|
||||
+ " age: \"40\"\n"
|
||||
+ " matchers:\n"
|
||||
+ " headers:\n"
|
||||
+ " - key: \"Content-Type\"\n"
|
||||
+ " regex: \"application/json.*\"\n"
|
||||
+ " regexType: \"as_string\"\n"
|
||||
+ "name: \"shouldRetrieveOldEnoughPerson\"\n"
|
||||
+ "metadata:\n"
|
||||
+ " verifier:\n"
|
||||
+ " tool: \"graphql\"\n";
|
||||
// @formatter:on
|
||||
|
||||
@Test
|
||||
void should_not_match_when_unsupported_tool() {
|
||||
BDDAssertions.then(new GraphQlMatcher().isApplicable("unknown")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_match_when_the_graphql_part_matches_regardless_of_whitespace_entries_in_the_query() {
|
||||
GraphQlMatcher matcher = new GraphQlMatcher();
|
||||
|
||||
MatchResult result = matcher.match(
|
||||
YamlContractConverter.INSTANCE.read(PROPER_YAML.getBytes()), request(),
|
||||
null);
|
||||
|
||||
BDDAssertions.then(result.isExactMatch()).isTrue();
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
private static final String NOT_MATCHING_QUERY_BODY = "{\n"
|
||||
+ "\"query\":\"this should not match\",\n"
|
||||
+ "\"variables\":{\"personName\":\"Old Enough\"},\n"
|
||||
+ "\"operationName\":\"queryName\"\n"
|
||||
+ "}";
|
||||
// @formatter:on
|
||||
|
||||
@Test
|
||||
void should_not_match_when_the_query_does_not_match() {
|
||||
GraphQlMatcher matcher = new GraphQlMatcher();
|
||||
|
||||
MatchResult result = matcher.match(
|
||||
YamlContractConverter.INSTANCE.read(PROPER_YAML.getBytes()),
|
||||
request(NOT_MATCHING_QUERY_BODY), null);
|
||||
|
||||
BDDAssertions.then(result.isExactMatch()).isFalse();
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
private static final String NOT_MATCHING_VARIABLES_BODY = "{\n"
|
||||
+ "\"query\":\"query queryName($personName: String!) {\\n personToCheck(name: $personName) {\\n name\\n age\\n }\\n}\\n\\n\\n\\n\",\n"
|
||||
+ "\"variables\":{\"Not matching key\":\"Not matching value\"},\n"
|
||||
+ "\"operationName\":\"queryName\"\n"
|
||||
+ "}";
|
||||
// @formatter:on
|
||||
|
||||
@Test
|
||||
void should_not_match_when_the_variables_does_not_match() {
|
||||
GraphQlMatcher matcher = new GraphQlMatcher();
|
||||
|
||||
MatchResult result = matcher.match(
|
||||
YamlContractConverter.INSTANCE.read(PROPER_YAML.getBytes()),
|
||||
request(NOT_MATCHING_VARIABLES_BODY), null);
|
||||
|
||||
BDDAssertions.then(result.isExactMatch()).isFalse();
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
private static final String NOT_MATCHING_OPERATION_NAME_BODY = "{\n"
|
||||
+ "\"query\":\"query queryName($personName: String!) {\\n personToCheck(name: $personName) {\\n name\\n age\\n }\\n}\\n\\n\\n\\n\",\n"
|
||||
+ "\"variables\":{\"personName\":\"Old Enough\"},\n"
|
||||
+ "\"operationName\":\"not matching operation name\"\n"
|
||||
+ "}";
|
||||
// @formatter:on
|
||||
|
||||
@Test
|
||||
void should_not_match_when_the_operation_name_does_not_match() {
|
||||
GraphQlMatcher matcher = new GraphQlMatcher();
|
||||
|
||||
MatchResult result = matcher.match(
|
||||
YamlContractConverter.INSTANCE.read(PROPER_YAML.getBytes()),
|
||||
request(NOT_MATCHING_OPERATION_NAME_BODY), null);
|
||||
|
||||
BDDAssertions.then(result.isExactMatch()).isFalse();
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
private static final String REQUEST_BODY = "{\n"
|
||||
+ "\"query\":\"query queryName($personName: String!) {\\n personToCheck(name: $personName) {\\n name\\n age\\n }\\n}\\n\\n\\n\\n\",\n"
|
||||
+ "\"variables\":{\"personName\":\"Old Enough\"},\n"
|
||||
+ "\"operationName\":\"queryName\"\n"
|
||||
+ "}";
|
||||
// @formatter:on
|
||||
|
||||
private Request request() {
|
||||
return request(REQUEST_BODY);
|
||||
}
|
||||
|
||||
private Request request(String body) {
|
||||
Request request = BDDMockito.mock(Request.class);
|
||||
BDDMockito.given(request.getBody()).willReturn(body.getBytes());
|
||||
return request;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright 2020-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.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.dsl.wiremock;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.github.tomakehurst.wiremock.extension.Parameters;
|
||||
import com.github.tomakehurst.wiremock.http.Request;
|
||||
import com.github.tomakehurst.wiremock.matching.MatchResult;
|
||||
import org.assertj.core.api.BDDAssertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.BDDMockito;
|
||||
import reactor.util.function.Tuple2;
|
||||
import reactor.util.function.Tuples;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
|
||||
class SpringCloudContractRequestMatcherTests {
|
||||
|
||||
@Test
|
||||
void should_not_match_when_contract_missing() {
|
||||
SpringCloudContractRequestMatcher matcher = new SpringCloudContractRequestMatcher() {
|
||||
|
||||
@Override
|
||||
List<RequestMatcher> matchers() {
|
||||
return Collections.singletonList((contracts, request, parameters) -> {
|
||||
throw new UnsupportedOperationException("This should not be called");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
MatchResult result = matcher.match(BDDMockito.mock(Request.class),
|
||||
Parameters.one("tool", "foo"));
|
||||
|
||||
BDDAssertions.then(result.isExactMatch()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_not_match_when_tool_missing() {
|
||||
SpringCloudContractRequestMatcher matcher = new SpringCloudContractRequestMatcher() {
|
||||
|
||||
@Override
|
||||
List<RequestMatcher> matchers() {
|
||||
return Collections.singletonList((contracts, request, parameters) -> {
|
||||
throw new UnsupportedOperationException("This should not be called");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
MatchResult result = matcher.match(BDDMockito.mock(Request.class),
|
||||
Parameters.one("contract", "value"));
|
||||
|
||||
BDDAssertions.then(result.isExactMatch()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_not_match_when_unsupported_tool() {
|
||||
SpringCloudContractRequestMatcher matcher = new SpringCloudContractRequestMatcher() {
|
||||
|
||||
@Override
|
||||
List<RequestMatcher> matchers() {
|
||||
return Collections.singletonList((contracts, request, parameters) -> {
|
||||
throw new UnsupportedOperationException("This should not be called");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
MatchResult result = matcher.match(BDDMockito.mock(Request.class),
|
||||
Parameters.one("contract", "value"));
|
||||
|
||||
BDDAssertions.then(result.isExactMatch()).isFalse();
|
||||
}
|
||||
|
||||
private static final String PROPER_YAML = "---\n"
|
||||
+ "request:\n"
|
||||
+ " method: \"POST\"\n"
|
||||
+ " url: \"/graphql\"\n"
|
||||
+ " headers:\n"
|
||||
+ " Content-Type: \"application/json\"\n"
|
||||
+ " body:\n"
|
||||
+ " query: \"query queryName($personName: String!) { personToCheck(name: $personName)"
|
||||
+ " { name age } }\"\n"
|
||||
+ " variables:\n"
|
||||
+ " personName: \"Old Enough\"\n"
|
||||
+ " operationName: \"queryName\"\n"
|
||||
+ " matchers:\n"
|
||||
+ " headers:\n"
|
||||
+ " - key: \"Content-Type\"\n"
|
||||
+ " regex: \"application/json.*\"\n"
|
||||
+ " regexType: \"as_string\"\n"
|
||||
+ "response:\n"
|
||||
+ " status: 200\n"
|
||||
+ " headers:\n"
|
||||
+ " Content-Type: \"application/json\"\n"
|
||||
+ " body:\n"
|
||||
+ " data:\n"
|
||||
+ " personToCheck:\n"
|
||||
+ " name: \"Old Enough\"\n"
|
||||
+ " age: \"40\"\n"
|
||||
+ " matchers:\n"
|
||||
+ " headers:\n"
|
||||
+ " - key: \"Content-Type\"\n"
|
||||
+ " regex: \"application/json.*\"\n"
|
||||
+ " regexType: \"as_string\"\n"
|
||||
+ "name: \"shouldRetrieveOldEnoughPerson\"\n"
|
||||
+ "metadata:\n"
|
||||
+ " verifier:\n"
|
||||
+ " tool: \"graphql\"\n";
|
||||
|
||||
@Test
|
||||
void should_not_match_when_exception_occurs_while_trying_to_parse_contract() {
|
||||
SpringCloudContractRequestMatcher matcher = new SpringCloudContractRequestMatcher() {
|
||||
@Override
|
||||
List<RequestMatcher> matchers() {
|
||||
return Collections.singletonList((contracts, request, parameters) -> {
|
||||
throw new UnsupportedOperationException("This should not be called");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
MatchResult result = matcher.match(BDDMockito.mock(Request.class),
|
||||
toMap(Tuples.of("tool", "unsupported"), Tuples.of("contract", PROPER_YAML)));
|
||||
|
||||
BDDAssertions.then(result.isExactMatch()).isFalse();
|
||||
}
|
||||
// @formatter:off
|
||||
// @formatter:on
|
||||
|
||||
@Test
|
||||
void should_delegate_to_an_applicable_request_matcher() {
|
||||
SpringCloudContractRequestMatcher matcher = new SpringCloudContractRequestMatcher() {
|
||||
@Override
|
||||
List<RequestMatcher> matchers() {
|
||||
return Collections.singletonList(new ApplicableRequestMatcher());
|
||||
}
|
||||
};
|
||||
|
||||
MatchResult result = matcher.match(BDDMockito.mock(Request.class),
|
||||
toMap(Tuples.of("tool", "graphql"), Tuples.of("contract", PROPER_YAML)));
|
||||
|
||||
BDDAssertions.then(result.isExactMatch()).isTrue();
|
||||
}
|
||||
|
||||
private Parameters toMap(Tuple2<String, Object>... tuple2) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
for (Tuple2<String, Object> tuple : tuple2) {
|
||||
map.put(tuple.getT1(), tuple.getT2());
|
||||
}
|
||||
return Parameters.from(map);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ApplicableRequestMatcher implements RequestMatcher {
|
||||
|
||||
@Override
|
||||
public MatchResult match(List<YamlContract> contracts, Request request,
|
||||
Parameters parameters) {
|
||||
return MatchResult.of(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(String tool) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user