Added GraphQL support (#1506)

fixes #670
This commit is contained in:
Marcin Grzejszczak
2020-09-11 09:32:11 +02:00
committed by GitHub
parent 2c5b71222a
commit a2fac97148
19 changed files with 1135 additions and 132 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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";
}
/**

View File

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

View File

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

View File

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