├── src
├── main
│ ├── resources
│ │ ├── application.properties
│ │ └── META-INF
│ │ │ └── spring.factories
│ ├── java
│ │ └── guru
│ │ │ └── springframework
│ │ │ └── scc
│ │ │ └── oa3
│ │ │ ├── package-info.java
│ │ │ └── Placeholder.java
│ └── groovy
│ │ └── org
│ │ └── springframework
│ │ └── cloud
│ │ └── contract
│ │ ├── stubrunner
│ │ └── OpenApiStubDownloaderBuilder.groovy
│ │ └── verifier
│ │ └── converter
│ │ └── OpenApiContractConverter.groovy
└── test
│ ├── resources
│ ├── yml
│ │ ├── request.json
│ │ ├── response.json
│ │ ├── contract_from_file.yml
│ │ ├── contract_message_scenario3.yml
│ │ ├── multiple_contracts.yml
│ │ ├── contract_broken_response_headers.yml
│ │ ├── contract_message_scenario1.yml
│ │ ├── contract_message_scenario2.yml
│ │ ├── README.md
│ │ ├── contract_message_method.yml
│ │ ├── contract_message_input_message.yml
│ │ ├── contract_cookies.yml
│ │ ├── contract_multipart.yml
│ │ ├── contract_message.yml
│ │ ├── contract_reference_request.yml
│ │ ├── contract_broken_request_headers.yml
│ │ ├── contract_rest.yml
│ │ ├── contract_rest_with_path.yml
│ │ ├── contract.yml
│ │ ├── contract_message_matchers.yml
│ │ └── contract_matchers.yml
│ ├── logback-test.xml
│ └── openapi
│ │ ├── fraudservice.yaml
│ │ ├── contract_OA3.yml
│ │ ├── contract_OA3_contractPath.yml
│ │ ├── openapi_petstore.yml
│ │ ├── openapi.yml
│ │ ├── contract_matchers.yml
│ │ └── payor_example.yml
│ ├── java
│ └── guru
│ │ └── springframework
│ │ └── scc
│ │ └── oa3
│ │ └── SpringCloudContractOa3ApplicationTests.java
│ └── groovy
│ └── org
│ └── springframework
│ └── cloud
│ └── contract
│ └── verifier
│ └── converter
│ ├── ServiceNameTest.groovy
│ ├── DslContracts.groovy
│ └── OpenApiContactConverterTest.groovy
├── .circleci
├── public_key.asc.enc
├── private_key.asc.enc
├── Dockerfile
├── .circleci.settings.xml
└── config.yml
├── .mvn
└── wrapper
│ ├── maven-wrapper.jar
│ └── maven-wrapper.properties
├── .gitignore
├── mvnw.cmd
├── mvnw
├── LICENSE
├── pom.xml
└── README.md
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/test/resources/yml/request.json:
--------------------------------------------------------------------------------
1 | { "hello" : "request" }
--------------------------------------------------------------------------------
/src/test/resources/yml/response.json:
--------------------------------------------------------------------------------
1 | { "hello" : "response" }
--------------------------------------------------------------------------------
/.circleci/public_key.asc.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/springframeworkguru/spring-cloud-contract-oa3/HEAD/.circleci/public_key.asc.enc
--------------------------------------------------------------------------------
/.circleci/private_key.asc.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/springframeworkguru/spring-cloud-contract-oa3/HEAD/.circleci/private_key.asc.enc
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/springframeworkguru/spring-cloud-contract-oa3/HEAD/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/src/main/java/guru/springframework/scc/oa3/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * @author John Thompson
3 | */
4 | package guru.springframework.scc.oa3;
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.3/apache-maven-3.5.3-bin.zip
2 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/spring.factories:
--------------------------------------------------------------------------------
1 | org.springframework.cloud.contract.spec.ContractConverter=\
2 | org.springframework.cloud.contract.verifier.converter.OpenApiContractConverter
--------------------------------------------------------------------------------
/src/test/resources/yml/contract_from_file.yml:
--------------------------------------------------------------------------------
1 | request:
2 | method: GET
3 | url: /foo
4 | bodyFromFile: request.json
5 | response:
6 | status: 200
7 | bodyFromFile: response.json
--------------------------------------------------------------------------------
/src/test/resources/yml/contract_message_scenario3.yml:
--------------------------------------------------------------------------------
1 | label: some_label
2 | input:
3 | messageFrom: jms:delete
4 | messageBody:
5 | bookName: 'foo'
6 | messageHeaders:
7 | sample: header
8 | assertThat: bookWasDeleted()
9 |
--------------------------------------------------------------------------------
/src/main/groovy/org/springframework/cloud/contract/stubrunner/OpenApiStubDownloaderBuilder.groovy:
--------------------------------------------------------------------------------
1 | package org.springframework.cloud.contract.stubrunner
2 |
3 | /**
4 | * Created by jt on 5/24/18.
5 | */
6 | class OpenApiStubDownloaderBuilder {
7 | }
8 |
--------------------------------------------------------------------------------
/src/test/resources/yml/multiple_contracts.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: should post a user
3 | request:
4 | method: POST
5 | url: /users/1
6 | response:
7 | status: 200
8 |
9 | ---
10 | request:
11 | method: POST
12 | url: /users/2
13 | response:
14 | status: 200
--------------------------------------------------------------------------------
/src/test/resources/yml/contract_broken_response_headers.yml:
--------------------------------------------------------------------------------
1 | request:
2 | url: /foo
3 | method: PUT
4 | response:
5 | status: 200
6 | headers:
7 | foo2: bar
8 | fooRes: baz
9 | matchers:
10 | headers:
11 | - key: foo2
12 | regex: barrrr
--------------------------------------------------------------------------------
/src/test/resources/yml/contract_message_scenario1.yml:
--------------------------------------------------------------------------------
1 | label: some_label
2 | input:
3 | triggeredBy: bookReturnedTriggered
4 | outputMessage:
5 | sentTo: activemq:output
6 | body:
7 | bookName: foo
8 | headers:
9 | BOOK-NAME: foo
10 | contentType: application/json
11 |
--------------------------------------------------------------------------------
/src/test/resources/yml/contract_message_scenario2.yml:
--------------------------------------------------------------------------------
1 | label: some_label
2 | input:
3 | messageFrom: jms:input
4 | messageBody:
5 | bookName: 'foo'
6 | messageHeaders:
7 | sample: header
8 | outputMessage:
9 | sentTo: jms:output
10 | body:
11 | bookName: foo
12 | headers:
13 | BOOK-NAME: foo
14 |
--------------------------------------------------------------------------------
/src/main/java/guru/springframework/scc/oa3/Placeholder.java:
--------------------------------------------------------------------------------
1 | package guru.springframework.scc.oa3;
2 |
3 | /**
4 | * Empty class for Java Doc - to keep Javadoc happy. Hit problems with
5 | * Groovy doc - this is a workaround to dependency hell.
6 | *
7 | * Created by jt on 2018-12-18.
8 | */
9 | public class Placeholder {
10 | }
11 |
--------------------------------------------------------------------------------
/src/test/java/guru/springframework/scc/oa3/SpringCloudContractOa3ApplicationTests.java:
--------------------------------------------------------------------------------
1 | package guru.springframework.scc.oa3;
2 |
3 | import org.junit.Test;
4 |
5 | //@RunWith(SpringRunner.class)
6 | //@SpringBootTest
7 | public class SpringCloudContractOa3ApplicationTests {
8 |
9 | @Test
10 | public void contextLoads() {
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/src/test/resources/yml/README.md:
--------------------------------------------------------------------------------
1 | ## YAML Contracts
2 |
3 | The YAML files in this directory were copied from the SSC project and are used for
4 | testing the parsing of SSC contracts defined in YML.
5 |
6 | These files are here for reference only. The OA3 converter should have the same capabilities as the YAML DSL.
7 |
8 | OA3 versions may be found in ../openapi.
--------------------------------------------------------------------------------
/.circleci/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM circleci/openjdk:8-jdk
2 |
3 | COPY private_key.asc.enc /private_key.asc.enc
4 | COPY public_key.asc.enc /public_key.asc.enc
5 |
6 | RUN sudo apt-get install gnupg2 -y
7 |
8 | CMD mkdir ${HOME}/.gnupg | \
9 | openssl aes-256-cbc -d -in /public_key.asc.enc -out ${HOME}/.gnupg/pubring.gpg -k ${ENC_PASS} | \
10 | openssl aes-256-cbc -d -in /private_key.asc.enc -out ${HOME}/.gnupg/secring.gpg -k ${ENC_PASS} | \
11 | tail -f /dev/null
--------------------------------------------------------------------------------
/.circleci/.circleci.settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ossrh
5 | john@springframework.guru
6 | ${env.OSSRH_PWD}
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/test/resources/yml/contract_message_method.yml:
--------------------------------------------------------------------------------
1 | # Human readable description
2 | description: Some description
3 | # Label by means of which the output message can be triggered
4 | label: some_label
5 | input:
6 | # the contract will be triggered by a method
7 | triggeredBy: bookReturnedTriggered()
8 | # output message of the contract
9 | outputMessage:
10 | # destination to which the output message will be sent
11 | sentTo: output
12 | # the body of the output message
13 | body:
14 | bookName: foo
15 | # the headers of the output message
16 | headers:
17 | BOOK-NAME: foo
--------------------------------------------------------------------------------
/src/test/resources/yml/contract_message_input_message.yml:
--------------------------------------------------------------------------------
1 | # Human readable description
2 | description: Some description
3 | # Label by means of which the output message can be triggered
4 | label: some_label
5 | # input is a message
6 | input:
7 | messageFrom: input
8 | # has the following body
9 | messageBody:
10 | bookName: 'foo'
11 | # and the following headers
12 | messageHeaders:
13 | sample: 'header'
14 | # output message of the contract
15 | outputMessage:
16 | # destination to which the output message will be sent
17 | sentTo: output
18 | # the body of the output message
19 | body:
20 | bookName: foo
21 | # the headers of the output message
22 | headers:
23 | BOOK-NAME: foo
--------------------------------------------------------------------------------
/src/test/resources/yml/contract_cookies.yml:
--------------------------------------------------------------------------------
1 | description: Contract with cookies
2 | name: cookies-contract
3 | priority: 1
4 | ignored: true
5 | request:
6 | method: PUT
7 | url: /foo
8 | cookies:
9 | foo: bar
10 | fooRegex: reg
11 | fooPredefinedRegex: true
12 | matchers:
13 | cookies:
14 | - key: fooRegex
15 | regex: reg
16 | - key: fooPredefinedRegex
17 | predefined: any_boolean
18 | response:
19 | status: 200
20 | cookies:
21 | foo: baz
22 | fooRegex: 123
23 | source: ip_address
24 | fooPredefinedRegex: true
25 | body:
26 | status: OK
27 | matchers:
28 | cookies:
29 | - key: fooRegex
30 | regex: "[0-9]+"
31 | - key: source
32 | regex: ip_address
33 | - key: fooPredefinedRegex
34 | predefined: any_boolean
35 |
--------------------------------------------------------------------------------
/src/test/resources/yml/contract_multipart.yml:
--------------------------------------------------------------------------------
1 | request:
2 | method: PUT
3 | url: /multipart
4 | headers:
5 | Content-Type: multipart/form-data;boundary=AaB03x
6 | multipart:
7 | params:
8 | # key (parameter name), value (parameter value) pair
9 | formParameter: '"formParameterValue"'
10 | someBooleanParameter: true
11 | named:
12 | - paramName: file
13 | fileName: filename.csv
14 | fileContent: file content
15 | matchers:
16 | multipart:
17 | params:
18 | - key: formParameter
19 | regex: ".+"
20 | - key: someBooleanParameter
21 | predefined: any_boolean
22 | named:
23 | - paramName: file
24 | fileName:
25 | predefined: non_empty
26 | fileContent:
27 | predefined: non_empty
28 | response:
29 | status: 200
--------------------------------------------------------------------------------
/src/test/resources/yml/contract_message.yml:
--------------------------------------------------------------------------------
1 | description: Some description
2 | label: some_label
3 | name: some name
4 | ignored: true
5 | input:
6 | messageFrom: foo
7 | triggeredBy: foo()
8 | messageHeaders:
9 | foo: bar
10 | messageBody:
11 | foo: bar
12 | assertThat: bar()
13 | matchers:
14 | body:
15 | - path: $.bar
16 | type: by_regex
17 | value: bar
18 | headers:
19 | - key: foo
20 | regex: bar
21 | outputMessage:
22 | sentTo: bar
23 | headers:
24 | foo2: bar
25 | foo3: bar3
26 | fooRes: baz
27 | body:
28 | foo2: bar
29 | foo3: baz
30 | assertThat: baz()
31 | matchers:
32 | body:
33 | - path: $.foo2
34 | type: by_regex
35 | value: bar
36 | - path: $.foo3
37 | type: by_command
38 | value: executeMe($it)
39 | headers:
40 | - key: foo2
41 | regex: bar
42 | - key: foo3
43 | command: andMeToo($it)
44 |
--------------------------------------------------------------------------------
/src/test/resources/yml/contract_reference_request.yml:
--------------------------------------------------------------------------------
1 | request:
2 | method: GET
3 | url: /api/v1/xxxx
4 | queryParameters:
5 | foo:
6 | - bar
7 | - bar2
8 | headers:
9 | Authorization:
10 | - secret
11 | - secret2
12 | body:
13 | foo: bar
14 | baz: 5
15 | response:
16 | status: 200
17 | headers:
18 | Authorization: "foo {{{ request.headers.Authorization.0 }}} bar"
19 | body:
20 | url: "{{{ request.url }}}"
21 | path: "{{{ request.path }}}"
22 | pathIndex: "{{{ request.path.1 }}}"
23 | param: "{{{ request.query.foo }}}"
24 | paramIndex: "{{{ request.query.foo.1 }}}"
25 | authorization: "{{{ request.headers.Authorization.0 }}}"
26 | authorization2: "{{{ request.headers.Authorization.1 }}"
27 | fullBody: "{{{ request.body }}}"
28 | responseFoo: "{{{ jsonpath this '$.foo' }}}"
29 | responseBaz: "{{{ jsonpath this '$.baz' }}}"
30 | responseBaz2: "Bla bla {{{ jsonpath this '$.foo' }}} bla bla"
--------------------------------------------------------------------------------
/src/test/resources/yml/contract_broken_request_headers.yml:
--------------------------------------------------------------------------------
1 | request:
2 | url: /foo
3 | queryParameters:
4 | a: b
5 | b: c
6 | method: PUT
7 | headers:
8 | foo: bar
9 | fooReq: baz
10 | body:
11 | foo: bar
12 | matchers:
13 | body:
14 | - path: $.foo
15 | type: by_regex
16 | value: bar
17 | headers:
18 | - key: foo
19 | regex: barrrr
20 | response:
21 | status: 200
22 | headers:
23 | foo2: bar
24 | fooRes: baz
25 | body:
26 | foo2: bar
27 | cookies:
28 | foo: baz
29 | fooRegex: 123
30 | source: ip_address
31 | fooPredefinedRegex: true
32 | matchers:
33 | body:
34 | - path: $.foo2
35 | type: by_regex
36 | value: bar
37 | headers:
38 | - key: foo2
39 | regex: bar
40 | cookies:
41 | - key: fooRegex
42 | regex: "[0-9]+"
43 | - key: source
44 | regex: ip_address
45 | - key: fooPredefinedRegex
46 | predefined: any_boolean
--------------------------------------------------------------------------------
/src/test/resources/yml/contract_rest.yml:
--------------------------------------------------------------------------------
1 | description: Some description
2 | name: some name
3 | priority: 8
4 | ignored: true
5 | request:
6 | url: /foo
7 | queryParameters:
8 | a: b
9 | b: c
10 | method: PUT
11 | headers:
12 | foo: bar
13 | fooReq: baz
14 | body:
15 | foo: bar
16 | matchers:
17 | body:
18 | - path: $.foo
19 | type: by_regex
20 | value: bar
21 | headers:
22 | - key: foo
23 | regex: bar
24 | response:
25 | status: 200
26 | headers:
27 | foo2: bar
28 | foo3: foo33
29 | fooRes: baz
30 | body:
31 | foo2: bar
32 | foo3: baz
33 | nullValue: null
34 | matchers:
35 | body:
36 | - path: $.foo2
37 | type: by_regex
38 | value: bar
39 | - path: $.foo3
40 | type: by_command
41 | value: executeMe($it)
42 | - path: $.nullValue
43 | type: by_null
44 | value: null
45 | headers:
46 | - key: foo2
47 | regex: bar
48 | - key: foo3
49 | command: andMeToo($it)
--------------------------------------------------------------------------------
/src/test/resources/yml/contract_rest_with_path.yml:
--------------------------------------------------------------------------------
1 | description: Some description
2 | name: some name
3 | priority: 8
4 | ignored: true
5 | #tag::url_path[]
6 | request:
7 | method: PUT
8 | urlPath: /foo
9 | #end::url_path[]
10 | queryParameters:
11 | a: b
12 | b: c
13 | headers:
14 | foo: bar
15 | fooReq: baz
16 | body:
17 | foo: bar
18 | matchers:
19 | body:
20 | - path: $.foo
21 | type: by_regex
22 | value: bar
23 | headers:
24 | - key: foo
25 | regex: bar
26 | response:
27 | status: 200
28 | headers:
29 | foo2: bar
30 | foo3: foo33
31 | fooRes: baz
32 | body:
33 | foo2: bar
34 | foo3: baz
35 | nullValue: null
36 | matchers:
37 | body:
38 | - path: $.foo2
39 | type: by_regex
40 | value: bar
41 | - path: $.foo3
42 | type: by_command
43 | value: executeMe($it)
44 | - path: $.nullValue
45 | type: by_null
46 | value: null
47 | headers:
48 | - key: foo2
49 | regex: bar
50 | - key: foo3
51 | command: andMeToo($it)
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Java Maven CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-java/ for more details
4 | #
5 | version: 2
6 | jobs:
7 | build:
8 | docker:
9 | # specify the version you desire here
10 | - image: circleci/openjdk:11-jdk-browsers-legacy
11 |
12 | # Specify service dependencies here if necessary
13 | # CircleCI maintains a library of pre-built images
14 | # documented at https://circleci.com/docs/2.0/circleci-images/
15 | # - image: circleci/postgres:9.4
16 |
17 | working_directory: ~/repo
18 |
19 | environment:
20 | # Customize the JVM maximum heap limit
21 | MAVEN_OPTS: -Xmx3200m
22 |
23 | steps:
24 |
25 | # - run: gpg2 --list-keys
26 |
27 | - checkout
28 |
29 | # Download and cache dependencies
30 | - restore_cache:
31 | keys:
32 | - v1-dependencies-{{ checksum "pom.xml" }}
33 | # fallback to using the latest cache if no exact match is found
34 | - v1-dependencies-
35 |
36 | - run: mvn dependency:go-offline
37 |
38 | - save_cache:
39 | paths:
40 | - ~/.m2
41 | key: v1-dependencies-{{ checksum "pom.xml" }}
42 |
43 | - run: mvn package sonar:sonar -Dsonar.organization=springframeworkguru-github -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=${SONAR_PAS}
44 |
45 | # - run: |
46 | # curl -fsSL https://git.io/vpDIq | \
47 | # bash -s -- ${PUB_URL} ${ENC_PASS}
48 |
49 | # - run: |
50 | # curl -fsSL https://git.io/vpDIq | \
51 | # bash -s -- ${PRV_URL} ${ENC_PASS}
52 |
53 | # - run: mvn -s .circleci.settings.xml deploy
--------------------------------------------------------------------------------
/src/test/resources/yml/contract.yml:
--------------------------------------------------------------------------------
1 | #tag::description[]
2 | description: Some description
3 | #end::description[]
4 | #tag::name[]
5 | name: some name
6 | #end::name[]
7 | #tag::priority[]
8 | priority: 8
9 | #end::priority[]
10 | #tag::ignored[]
11 | ignored: true
12 | #end::ignored[]
13 | #tag::request[]
14 | request:
15 | #end::request[]
16 | #tag::request_obligatory[]
17 | method: PUT
18 | url: /foo
19 | #end::request_obligatory[]
20 | #tag::query_params[]
21 | queryParameters:
22 | a: b
23 | b: c
24 | #tag::query_params[]
25 | #tag::headers[]
26 | headers:
27 | foo: bar
28 | fooReq: baz
29 | #end::headers[]
30 | #tag::cookies[]
31 | cookies:
32 | foo: bar
33 | fooReq: baz
34 | #end::cookies[]
35 | #tag::body[]
36 | body:
37 | foo: bar
38 | #end::body[]
39 | #tag::request_matcher[]
40 | matchers:
41 | body:
42 | - path: $.foo
43 | type: by_regex
44 | value: bar
45 | headers:
46 | - key: foo
47 | regex: bar
48 | #end::request_matcher[]
49 | #tag::response[]
50 | response:
51 | #end::response[]
52 | #tag::response_obligatory[]
53 | status: 200
54 | #end::response_obligatory[]
55 | headers:
56 | foo2: bar
57 | foo3: foo33
58 | fooRes: baz
59 | body:
60 | foo2: bar
61 | foo3: baz
62 | nullValue: thisisnull
63 | matchers:
64 | body:
65 | - path: $.foo2
66 | type: by_regex
67 | value: bar
68 | - path: $.foo3
69 | type: by_command
70 | value: executeMe($it)
71 | - path: $.nullValue
72 | type: by_null
73 | value: null
74 | headers:
75 | - key: foo2
76 | regex: bar
77 | - key: foo3
78 | command: andMeToo($it)
79 | cookies:
80 | - key: foo2
81 | regex: bar
82 | - key: foo3
83 | predefined:
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ######################
2 | # Project Specific
3 | ######################
4 | /build/www/**
5 | /src/test/javascript/coverage/
6 | /src/test/javascript/PhantomJS*/
7 |
8 | ######################
9 | # Node
10 | ######################
11 | /node/
12 | node_tmp/
13 | node_modules/
14 | npm-debug.log.*
15 |
16 | ######################
17 | # SASS
18 | ######################
19 | .sass-cache/
20 |
21 | ######################
22 | # Eclipse
23 | ######################
24 | *.pydevproject
25 | .project
26 | .metadata
27 | tmp/
28 | tmp/**/*
29 | *.tmp
30 | *.bak
31 | *.swp
32 | *~.nib
33 | local.properties
34 | .classpath
35 | .settings/
36 | .loadpath
37 | .factorypath
38 | /src/main/resources/rebel.xml
39 |
40 | # External tool builders
41 | .externalToolBuilders/**
42 |
43 | # Locally stored "Eclipse launch configurations"
44 | *.launch
45 |
46 | # CDT-specific
47 | .cproject
48 |
49 | # PDT-specific
50 | .buildpath
51 |
52 | ######################
53 | # Intellij
54 | ######################
55 | .idea/
56 | *.iml
57 | *.iws
58 | *.ipr
59 | *.ids
60 | *.orig
61 |
62 | ######################
63 | # Visual Studio Code
64 | ######################
65 | .vscode/
66 |
67 | ######################
68 | # Maven
69 | ######################
70 | /log/
71 | /target/
72 | .flattened-pom.xml
73 |
74 | ######################
75 | # Gradle
76 | ######################
77 | .gradle/
78 | /build/
79 |
80 | ######################
81 | # Package Files
82 | ######################
83 | *.jar
84 | *.war
85 | *.ear
86 | *.db
87 |
88 | ######################
89 | # Windows
90 | ######################
91 | # Windows image file caches
92 | Thumbs.db
93 |
94 | # Folder config file
95 | Desktop.ini
96 |
97 | ######################
98 | # Mac OSX
99 | ######################
100 | .DS_Store
101 | .svn
102 |
103 | # Thumbnails
104 | ._*
105 |
106 | # Files that might appear on external disk
107 | .Spotlight-V100
108 | .Trashes
109 |
110 | ######################
111 | # Directories
112 | ######################
113 | /bin/
114 | /deploy/
115 |
116 | ######################
117 | # Logs
118 | ######################
119 | *.log
120 |
121 | ######################
122 | # Others
123 | ######################
124 | *.class
125 | *.*~
126 | *~
127 | .merge_file*
128 |
129 | ######################
130 | # Gradle Wrapper
131 | ######################
132 | !gradle/wrapper/gradle-wrapper.jar
133 |
134 | ######################
135 | # Maven Wrapper
136 | ######################
137 | !.mvn/wrapper/maven-wrapper.jar
138 |
139 | ######################
140 | # ESLint
141 | ######################
142 | .eslintcache
143 |
--------------------------------------------------------------------------------
/src/test/resources/yml/contract_message_matchers.yml:
--------------------------------------------------------------------------------
1 | label: card_rejected
2 | input:
3 | messageFrom: input
4 | messageBody:
5 | duck: 123
6 | alpha: "abc"
7 | number: 123
8 | aBoolean: true
9 | date: "2017-01-01"
10 | dateTime: "2017-01-01T01:23:45"
11 | time: "01:02:34"
12 | valueWithoutAMatcher: "foo"
13 | valueWithTypeMatch: "string"
14 | key:
15 | "complex.key": 'foo'
16 | messageHeaders:
17 | sample: 'header'
18 | contentType: application/json
19 | matchers:
20 | headers:
21 | - key: contentType
22 | regex: "application/json.*"
23 | body:
24 | - path: $.duck
25 | type: by_regex
26 | value: "[0-9]{3}"
27 | - path: $.duck
28 | type: by_equality
29 | - path: $.alpha
30 | type: by_regex
31 | predefined: only_alpha_unicode
32 | - path: $.alpha
33 | type: by_equality
34 | - path: $.number
35 | type: by_regex
36 | predefined: number
37 | - path: $.aBoolean
38 | type: by_regex
39 | predefined: any_boolean
40 | - path: $.date
41 | type: by_date
42 | - path: $.dateTime
43 | type: by_timestamp
44 | - path: $.time
45 | type: by_time
46 | - path: "$.['key'].['complex.key']"
47 | type: by_equality
48 | outputMessage:
49 | sentTo: channel
50 | body:
51 | duck: 123
52 | alpha: "abc"
53 | number: 123
54 | aBoolean: true
55 | date: "2017-01-01"
56 | dateTime: "2017-01-01T01:23:45"
57 | time: "01:02:34"
58 | valueWithoutAMatcher: "foo"
59 | valueWithTypeMatch: "string"
60 | valueWithMin:
61 | - 1
62 | - 2
63 | - 3
64 | valueWithMax:
65 | - 1
66 | - 2
67 | - 3
68 | valueWithMinMax:
69 | - 1
70 | - 2
71 | - 3
72 | valueWithMinEmpty: []
73 | valueWithMaxEmpty: []
74 | key:
75 | 'complex.key' : 'foo'
76 | matchers:
77 | headers:
78 | - key: Content-Type
79 | regex: "application/json.*"
80 | body:
81 | - path: $.duck
82 | type: by_regex
83 | value: "[0-9]{3}"
84 | - path: $.duck
85 | type: by_equality
86 | - path: $.alpha
87 | type: by_regex
88 | predefined: only_alpha_unicode
89 | - path: $.alpha
90 | type: by_equality
91 | - path: $.number
92 | type: by_regex
93 | predefined: number
94 | - path: $.aBoolean
95 | type: by_regex
96 | predefined: any_boolean
97 | - path: $.date
98 | type: by_date
99 | - path: $.dateTime
100 | type: by_timestamp
101 | - path: $.time
102 | type: by_time
103 | - path: $.valueWithTypeMatch
104 | type: by_type
105 | - path: $.valueWithMin
106 | type: by_type
107 | minOccurrence: 1
108 | - path: $.valueWithMax
109 | type: by_type
110 | maxOccurrence: 3
111 | - path: $.valueWithMinMax
112 | type: by_type
113 | minOccurrence: 1
114 | maxOccurrence: 3
115 | - path: $.valueWithMinEmpty
116 | type: by_type
117 | minOccurrence: 0
118 | - path: $.valueWithMaxEmpty
119 | type: by_type
120 | maxOccurrence: 0
121 | - path: $.duck
122 | type: by_command
123 | value: assertThatValueIsANumber($it)
124 | headers:
125 | contentType: application/json
--------------------------------------------------------------------------------
/src/test/resources/openapi/fraudservice.yaml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.0
2 | servers: []
3 | info:
4 | description: Example fraud detection service using consumer driven contracts
5 | version: "1.0.0"
6 | title: Fraud Detection Service
7 | contact:
8 | email: you@your-company.com
9 | license:
10 | name: Apache 2.0
11 | url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
12 | tags:
13 | - name: Fraud Service
14 | description: Fraud Service
15 | paths:
16 | /fraudcheck:
17 | put:
18 | tags:
19 | - Fraud Service
20 | summary: Checks client id for suspected Fraud
21 | operationId: fraudCheck
22 | description: For given client id and loan amount, service will examine client profile and determine request is too high
23 | # Define Contracts
24 | x-contracts:
25 | - contractId: 1
26 | name: Should Mark Client As Fraud
27 | - contractId: 2
28 | name: Test Loan Amount okay
29 | - contractId: 3
30 | name: Test missing loan amount
31 | requestBody:
32 | content:
33 | application/json:
34 | schema:
35 | $ref: '#/components/schemas/FraudCheckRequest'
36 | example: "{ 'clientId': '123456789', 'loanAmount': 999999 }"
37 | description: Client and Loan Amount to check
38 | # Define requests for each contract
39 | x-contracts:
40 | - contractId: 1
41 | body:
42 | "client.id" : 1452656883
43 | loanAmount: 99999
44 | matchers:
45 | body:
46 | - $ref: '#/components/x-matchers/clientMatcher' # reusable component
47 | - contractId: 2
48 | body:
49 | clientId: 1234567890
50 | loanAmount: 500
51 | matchers:
52 | body:
53 | - $ref: '#/components/x-matchers/clientMatcher'
54 | - contractId: 3
55 | body:
56 | clientId: 1234567890
57 | #missing loan amount
58 | responses:
59 | '200':
60 | description: okay
61 | content:
62 | application/json:
63 | schema:
64 | $ref: '#/components/schemas/FraudCheckResponse'
65 | example: "{ 'fraudCheckStatus': 'FRAUD', 'rejectionReason': 'Amount too high' }"
66 | # Define expected responses for contracts
67 | x-contracts:
68 | - contractId: 1
69 | # Status inferred from OpenAPI
70 | # Headers - contentType can be inferred from OpenAPI response object
71 | body:
72 | fraudCheckStatus: "FRAUD"
73 | rejectionReason: 'Amount too high'
74 | - contractId: 2
75 | body:
76 | fraudCheckStatus: "OK"
77 | '400':
78 | description: 'invalid input, object invalid'
79 | x-contracts:
80 | - contractId: 3
81 | components:
82 | schemas:
83 | FraudCheckRequest:
84 | type: object
85 | required:
86 | - clientId
87 | - loanAmount
88 | properties:
89 | clientId:
90 | type: string
91 | pattern: '[0-9]{10}'
92 | description: Client Id
93 | loanAmount:
94 | type: number
95 | FraudCheckResponse:
96 | type: object
97 | required:
98 | - fraudCheckStatus
99 | properties:
100 | fraudCheckStatus:
101 | type: string
102 | enum: [FRAUD, OKAY]
103 | rejectionReason:
104 | type: string
105 | description: Reason Message
106 | x-matchers:
107 | clientMatcher:
108 | path: $.['clientId']
109 | type: by_regex
110 | value: "[0-9]{10}"
--------------------------------------------------------------------------------
/src/test/resources/openapi/contract_OA3.yml:
--------------------------------------------------------------------------------
1 | openapi: "3.0.0"
2 | info:
3 | version: 1.0.0
4 | title: SCC
5 | paths:
6 | /foo:
7 | put:
8 | x-contracts:
9 | - contractId: 1
10 | description: Some description
11 | name: some name
12 | priority: 8
13 | ignored: true
14 | parameters:
15 | - name: a
16 | in: query
17 | schema:
18 | type: string
19 | x-contracts:
20 | - contractId: 1
21 | value: b
22 | - name: b
23 | in: query
24 | schema:
25 | type: string
26 | x-contracts:
27 | - contractId: 1
28 | value: c
29 | - name: foo
30 | in: header
31 | schema:
32 | type: string
33 | x-contracts:
34 | - contractId: 1
35 | value: bar
36 | - name: fooReq
37 | in: header
38 | schema:
39 | type: string
40 | x-contracts:
41 | - contractId: 1
42 | value: baz
43 | - name: foo
44 | in: cookie
45 | schema:
46 | type: string
47 | x-contracts:
48 | - contractId: 1
49 | value: bar
50 | - name: fooReq
51 | in: cookie
52 | schema:
53 | type: string
54 | x-contracts:
55 | - contractId: 1
56 | value: baz
57 | requestBody:
58 | content:
59 | application/json:
60 | schema:
61 | properties:
62 | foo:
63 | type: string
64 | x-contracts:
65 | - contractId: 1
66 | body:
67 | foo: bar
68 | matchers:
69 | body:
70 | - path: $.foo
71 | type: by_regex
72 | value: bar
73 | headers:
74 | - key: foo
75 | regex: bar
76 | responses:
77 | '200':
78 | description: the response
79 | content:
80 | application/json:
81 | schema:
82 | properties:
83 | foo:
84 | type: string
85 | x-contracts:
86 | - contractId: 1
87 | headers:
88 | foo2: bar
89 | foo3: foo33
90 | fooRes: baz
91 | body:
92 | foo2: bar
93 | foo3: baz
94 | nullValue: thisisnull
95 | matchers:
96 | body:
97 | - path: $.foo2
98 | type: by_regex
99 | value: bar
100 | - path: $.foo3
101 | type: by_command
102 | value: executeMe($it)
103 | - path: $.nullValue
104 | type: by_null
105 | value: null
106 | headers:
107 | - key: foo2
108 | regex: bar
109 | - key: foo3
110 | command: andMeToo($it)
111 | cookies:
112 | - key: foo2
113 | regex: bar
114 | - key: foo3
115 | predefined:
--------------------------------------------------------------------------------
/src/test/groovy/org/springframework/cloud/contract/verifier/converter/ServiceNameTest.groovy:
--------------------------------------------------------------------------------
1 | package org.springframework.cloud.contract.verifier.converter
2 |
3 | import spock.lang.Specification
4 |
5 |
6 | /**
7 | * Created by jt on 2019-04-02.
8 | */
9 | class ServiceNameTest extends Specification {
10 |
11 | URL petstoreUrl = OpenApiContactConverterTest.getResource("/openapi/openapi_petstore.yml")
12 | File petstoreFile = new File(petstoreUrl.toURI())
13 |
14 | OpenApiContractConverter contactConverter
15 |
16 | void setup() {
17 | contactConverter = new OpenApiContractConverter()
18 | }
19 |
20 | void cleanup() {
21 | System.properties.remove(OpenApiContractConverter.SERVICE_NAME_KEY)
22 | }
23 |
24 | def "Test Contract Not Set"(){
25 | when:
26 | def enabled = contactConverter.checkServiceEnabled(null)
27 |
28 | then:
29 | enabled
30 | }
31 |
32 | def "Test Contract Set, System Param Not Set"(){
33 | when:
34 | def enabled = contactConverter.checkServiceEnabled("FOO")
35 |
36 | then:
37 | enabled
38 | }
39 |
40 | def "Test Contract Set, Should Match one value"() {
41 | given:
42 | System.properties.setProperty(OpenApiContractConverter.SERVICE_NAME_KEY, "ServiceA")
43 |
44 | when:
45 | def enabled = contactConverter.checkServiceEnabled("ServiceA")
46 |
47 | then:
48 | enabled
49 | }
50 |
51 | def "Test Contract Set, Should NOT Match one value"() {
52 | given:
53 | System.properties.setProperty(OpenApiContractConverter.SERVICE_NAME_KEY, "ServiceA")
54 |
55 | when:
56 | def enabled = contactConverter.checkServiceEnabled("ServiceB")
57 |
58 | then:
59 | !enabled
60 | }
61 |
62 | def "Test Contract Set, Should Match List of Values"() {
63 | given:
64 | System.properties.setProperty(OpenApiContractConverter.SERVICE_NAME_KEY, "ServiceA,ServiceB")
65 |
66 | when:
67 | def enabled = contactConverter.checkServiceEnabled("ServiceB")
68 |
69 | then:
70 | enabled
71 | }
72 |
73 | def "Test Contract Set, Should Match List of Values with spaces"() {
74 | given:
75 | System.properties.setProperty(OpenApiContractConverter.SERVICE_NAME_KEY, "ServiceA, ServiceB")
76 |
77 | when:
78 | def enabled = contactConverter.checkServiceEnabled("ServiceB")
79 |
80 | then:
81 | enabled
82 | }
83 |
84 | def "testNoSystemparam"() {
85 | when:
86 | def contracts = contactConverter.convertFrom(petstoreFile)
87 |
88 | then:
89 | contracts
90 | contracts.size() == 3
91 | }
92 |
93 | def "test Service A"() {
94 | given:
95 | System.properties.setProperty(OpenApiContractConverter.SERVICE_NAME_KEY, "serviceA")
96 |
97 | when:
98 | def contracts = contactConverter.convertFrom(petstoreFile)
99 |
100 | then:
101 | contracts
102 | contracts.size() == 1
103 | }
104 |
105 | def "test Service A and B"() {
106 | given:
107 | System.properties.setProperty(OpenApiContractConverter.SERVICE_NAME_KEY, "serviceA, serviceB")
108 |
109 | when:
110 | def contracts = contactConverter.convertFrom(petstoreFile)
111 |
112 | then:
113 | contracts
114 | contracts.size() == 2
115 | }
116 |
117 | def "test Service A and C"() {
118 | given:
119 | System.properties.setProperty(OpenApiContractConverter.SERVICE_NAME_KEY, "serviceA, serviceC")
120 |
121 | when:
122 | def contracts = contactConverter.convertFrom(petstoreFile)
123 |
124 | then:
125 | contracts
126 | contracts.size() == 2
127 | }
128 |
129 | def "test Service A, B and C"() {
130 | given:
131 | System.properties.setProperty(OpenApiContractConverter.SERVICE_NAME_KEY, "serviceA,serviceB, serviceC")
132 |
133 | when:
134 | def contracts = contactConverter.convertFrom(petstoreFile)
135 |
136 | then:
137 | contracts
138 | contracts.size() == 3
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/test/resources/openapi/contract_OA3_contractPath.yml:
--------------------------------------------------------------------------------
1 | openapi: "3.0.0"
2 | info:
3 | version: 1.0.0
4 | title: SCC
5 | paths:
6 | /foo:
7 | put:
8 | x-contracts:
9 | - contractId: 1
10 | description: Some description
11 | name: some name
12 | priority: 8
13 | ignored: true
14 | contractPath: /foo1
15 | parameters:
16 | - name: a
17 | in: query
18 | schema:
19 | type: string
20 | x-contracts:
21 | - contractId: 1
22 | value: b
23 | - name: b
24 | in: query
25 | schema:
26 | type: string
27 | x-contracts:
28 | - contractId: 1
29 | value: c
30 | - name: foo
31 | in: header
32 | schema:
33 | type: string
34 | x-contracts:
35 | - contractId: 1
36 | value: bar
37 | - name: fooReq
38 | in: header
39 | schema:
40 | type: string
41 | x-contracts:
42 | - contractId: 1
43 | value: baz
44 | - name: foo
45 | in: cookie
46 | schema:
47 | type: string
48 | x-contracts:
49 | - contractId: 1
50 | value: bar
51 | - name: fooReq
52 | in: cookie
53 | schema:
54 | type: string
55 | x-contracts:
56 | - contractId: 1
57 | value: baz
58 | requestBody:
59 | content:
60 | application/json:
61 | schema:
62 | properties:
63 | foo:
64 | type: string
65 | x-contracts:
66 | - contractId: 1
67 | body:
68 | foo: bar
69 | matchers:
70 | body:
71 | - path: $.foo
72 | type: by_regex
73 | value: bar
74 | headers:
75 | - key: foo
76 | regex: bar
77 | responses:
78 | '200':
79 | description: the response
80 | content:
81 | application/json:
82 | schema:
83 | properties:
84 | foo:
85 | type: string
86 | x-contracts:
87 | - contractId: 1
88 | headers:
89 | foo2: bar
90 | foo3: foo33
91 | fooRes: baz
92 | body:
93 | foo2: bar
94 | foo3: baz
95 | nullValue: null
96 | matchers:
97 | body:
98 | - path: $.foo2
99 | type: by_regex
100 | value: bar
101 | - path: $.foo3
102 | type: by_command
103 | value: executeMe($it)
104 | - path: $.nullValue
105 | type: by_null
106 | value: null
107 | headers:
108 | - key: foo2
109 | regex: bar
110 | - key: foo3
111 | command: andMeToo($it)
112 | cookies:
113 | - key: foo2
114 | regex: bar
115 | - key: foo3
116 | predefined:
--------------------------------------------------------------------------------
/src/test/resources/openapi/openapi_petstore.yml:
--------------------------------------------------------------------------------
1 | openapi: "3.0.0"
2 | info:
3 | version: 1.0.0
4 | title: Swagger Petstore
5 | license:
6 | name: MIT
7 | servers:
8 | - url: http://petstore.swagger.io/v1
9 | paths:
10 | /pets:
11 | get:
12 | summary: List all pets
13 | operationId: listPets
14 | tags:
15 | - pets
16 | # Define Contracts
17 | x-contracts:
18 | - contractId: 1
19 | name: Test List All Pets, No Limit
20 | requestHeaders:
21 | - Content-type: application/json
22 | serviceName: serviceA
23 | - contractId: 2
24 | name: Test List All Pets, Limit 5
25 | requestHeaders:
26 | - Content-type: application/json
27 | serviceName: serviceB
28 | parameters:
29 | - name: limit
30 | in: query
31 | description: How many items to return at one time (max 100)
32 | required: false
33 | schema:
34 | type: integer
35 | format: int32
36 | # Define Parameters for Contracts
37 | x-contracts:
38 | - contractId: 1
39 | value: null
40 | - contractId: 2
41 | value: 5
42 | responses:
43 | '200':
44 | description: An paged array of pets
45 | headers:
46 | x-next:
47 | description: A link to the next page of responses
48 | schema:
49 | type: string
50 | content:
51 | application/json:
52 | schema:
53 | $ref: "#/components/schemas/Pets"
54 | #Define Expectations for Contracts (expecting 200, application/json response and below conditions)
55 | x-contracts:
56 | - contractId: 1
57 | matchers:
58 | path: $.*
59 | maxOccurance: 25
60 | - contractId: 2
61 | matchers:
62 | path: $.*
63 | maxOccurance: 25
64 | default:
65 | description: unexpected error
66 | content:
67 | application/json:
68 | schema:
69 | $ref: "#/components/schemas/Error"
70 | post:
71 | description: Creates a new pet in the store. Duplicates are allowed
72 | operationId: addPet
73 | # Define Contracts
74 | x-contracts:
75 | - contractId: 1
76 | name: Test Create Pet
77 | serviceName: serviceC
78 | requestHeaders:
79 | - Content-type: application/json
80 | requestBody:
81 | description: Pet to add to the store
82 | required: true
83 | content:
84 | application/json:
85 | schema:
86 | $ref: '#/components/schemas/NewPet'
87 | x-contracts:
88 | - contractId: 1
89 | body:
90 | name: Jake
91 | responses:
92 | '200':
93 | description: pet response
94 | content:
95 | application/json:
96 | schema:
97 | $ref: '#/components/schemas/Pet'
98 | default:
99 | description: unexpected error
100 | content:
101 | application/json:
102 | schema:
103 | $ref: '#/components/schemas/Error'
104 | /pets/{petId}:
105 | get:
106 | summary: Info for a specific pet
107 | operationId: showPetById
108 | tags:
109 | - pets
110 | parameters:
111 | - name: petId
112 | in: path
113 | required: true
114 | description: The id of the pet to retrieve
115 | schema:
116 | type: string
117 | responses:
118 | '200':
119 | description: Expected response to a valid request
120 | content:
121 | application/json:
122 | schema:
123 | $ref: "#/components/schemas/Pets"
124 | default:
125 | description: unexpected error
126 | content:
127 | application/json:
128 | schema:
129 | $ref: "#/components/schemas/Error"
130 | components:
131 | schemas:
132 | Pet:
133 | required:
134 | - id
135 | - name
136 | properties:
137 | id:
138 | type: integer
139 | format: int64
140 | name:
141 | type: string
142 | tag:
143 | type: string
144 | Pets:
145 | type: array
146 | items:
147 | $ref: "#/components/schemas/Pet"
148 | Error:
149 | required:
150 | - code
151 | - message
152 | properties:
153 | code:
154 | type: integer
155 | format: int32
156 | message:
157 | type: string
--------------------------------------------------------------------------------
/src/test/resources/yml/contract_matchers.yml:
--------------------------------------------------------------------------------
1 | request:
2 | method: GET
3 | urlPath: /get/1
4 | headers:
5 | Content-Type: application/json
6 | cookies:
7 | foo: 2
8 | bar: 3
9 | queryParameters:
10 | limit: 10
11 | offset: 20
12 | filter: 'email'
13 | sort: name
14 | search: 55
15 | age: 99
16 | name: John.Doe
17 | email: 'bob@email.com'
18 | body:
19 | duck: 123
20 | alpha: "abc"
21 | number: 123
22 | aBoolean: true
23 | date: "2017-01-01"
24 | dateTime: "2017-01-01T01:23:45"
25 | time: "01:02:34"
26 | valueWithoutAMatcher: "foo"
27 | valueWithTypeMatch: "string"
28 | key:
29 | "complex.key": 'foo'
30 | nullValue: null
31 | valueWithMin:
32 | - 1
33 | - 2
34 | - 3
35 | valueWithMax:
36 | - 1
37 | - 2
38 | - 3
39 | valueWithMinMax:
40 | - 1
41 | - 2
42 | - 3
43 | valueWithMinEmpty: []
44 | valueWithMaxEmpty: []
45 | matchers:
46 | url:
47 | regex: /get/[0-9]
48 | # predefined:
49 | # execute a method
50 | #command: 'equals($it)'
51 | queryParameters:
52 | - key: limit
53 | type: equal_to
54 | value: 20
55 | - key: offset
56 | type: containing
57 | value: 20
58 | - key: sort
59 | type: equal_to
60 | value: name
61 | - key: search
62 | type: not_matching
63 | value: '^[0-9]{2}$'
64 | - key: age
65 | type: not_matching
66 | value: '^\\w*$'
67 | - key: name
68 | type: matching
69 | value: 'John.*'
70 | - key: hello
71 | type: absent
72 | cookies:
73 | - key: foo
74 | regex: '[0-9]'
75 | - key: bar
76 | command: 'equals($it)'
77 | headers:
78 | - key: Content-Type
79 | regex: "application/json.*"
80 | body:
81 | - path: $.duck
82 | type: by_regex
83 | value: "[0-9]{3}"
84 | - path: $.duck
85 | type: by_equality
86 | - path: $.alpha
87 | type: by_regex
88 | predefined: only_alpha_unicode
89 | - path: $.alpha
90 | type: by_equality
91 | - path: $.number
92 | type: by_regex
93 | predefined: number
94 | - path: $.aBoolean
95 | type: by_regex
96 | predefined: any_boolean
97 | - path: $.date
98 | type: by_date
99 | - path: $.dateTime
100 | type: by_timestamp
101 | - path: $.time
102 | type: by_time
103 | - path: "$.['key'].['complex.key']"
104 | type: by_equality
105 | - path: $.nullvalue
106 | type: by_null
107 | - path: $.valueWithMin
108 | type: by_type
109 | minOccurrence: 1
110 | - path: $.valueWithMax
111 | type: by_type
112 | maxOccurrence: 3
113 | - path: $.valueWithMinMax
114 | type: by_type
115 | minOccurrence: 1
116 | maxOccurrence: 3
117 | response:
118 | status: 200
119 | cookies:
120 | foo: 1
121 | bar: 2
122 | body:
123 | duck: 123
124 | alpha: "abc"
125 | number: 123
126 | aBoolean: true
127 | date: "2017-01-01"
128 | dateTime: "2017-01-01T01:23:45"
129 | time: "01:02:34"
130 | valueWithoutAMatcher: "foo"
131 | valueWithTypeMatch: "string"
132 | valueWithMin:
133 | - 1
134 | - 2
135 | - 3
136 | valueWithMax:
137 | - 1
138 | - 2
139 | - 3
140 | valueWithMinMax:
141 | - 1
142 | - 2
143 | - 3
144 | valueWithMinEmpty: []
145 | valueWithMaxEmpty: []
146 | key:
147 | 'complex.key': 'foo'
148 | nulValue: null
149 | matchers:
150 | headers:
151 | - key: Content-Type
152 | regex: "application/json.*"
153 | cookies:
154 | - key: foo
155 | regex: '[0-9]'
156 | - key: bar
157 | command: 'equals($it)'
158 | body:
159 | - path: $.duck
160 | type: by_regex
161 | value: "[0-9]{3}"
162 | - path: $.duck
163 | type: by_equality
164 | - path: $.alpha
165 | type: by_regex
166 | predefined: only_alpha_unicode
167 | - path: $.alpha
168 | type: by_equality
169 | - path: $.number
170 | type: by_regex
171 | predefined: number
172 | - path: $.aBoolean
173 | type: by_regex
174 | predefined: any_boolean
175 | - path: $.date
176 | type: by_date
177 | - path: $.dateTime
178 | type: by_timestamp
179 | - path: $.time
180 | type: by_time
181 | - path: $.valueWithTypeMatch
182 | type: by_type
183 | - path: $.valueWithMin
184 | type: by_type
185 | minOccurrence: 1
186 | - path: $.valueWithMax
187 | type: by_type
188 | maxOccurrence: 3
189 | - path: $.valueWithMinMax
190 | type: by_type
191 | minOccurrence: 1
192 | maxOccurrence: 3
193 | - path: $.valueWithMinEmpty
194 | type: by_type
195 | minOccurrence: 0
196 | - path: $.valueWithMaxEmpty
197 | type: by_type
198 | maxOccurrence: 0
199 | - path: $.duck
200 | type: by_command
201 | value: assertThatValueIsANumber($it)
202 | - path: $.nullValue
203 | type: by_null
204 | value: null
205 | headers:
206 | Content-Type: application/json
--------------------------------------------------------------------------------
/src/test/groovy/org/springframework/cloud/contract/verifier/converter/DslContracts.groovy:
--------------------------------------------------------------------------------
1 | package org.springframework.cloud.contract.verifier.converter
2 |
3 | import org.springframework.cloud.contract.spec.Contract
4 |
5 | /**
6 | * Created by jt on 5/30/18.
7 | */
8 | class DslContracts {
9 | static Contract shouldMarkClientAsFraud(){
10 | return Contract.make {
11 | name = "1 - Should Mark Client As Fraud"
12 | request {
13 | method 'PUT'
14 | url '/fraudcheck'
15 | body([
16 | "client.id": $(regex('[0-9]{10}')),
17 | loanAmount: 99999
18 | ])
19 | headers {
20 | contentType('application/json')
21 | }
22 | }
23 | response {
24 | status OK()
25 | body([
26 | fraudCheckStatus: "FRAUD",
27 | "rejection.reason": "Amount too high"
28 | ])
29 | headers {
30 | contentType('application/json')
31 | }
32 | }
33 | }
34 | }
35 |
36 | static Contract shouldMarkClientasNotFraud() {
37 | return Contract.make {
38 | request {
39 | method 'PUT'
40 | url '/fraudcheck'
41 | body("""
42 | {
43 | "client.id":"${value(consumer(regex('[0-9]{10}')), producer('1234567890'))}",
44 | "loanAmount":123.123
45 | }
46 | """
47 | )
48 | headers {
49 | contentType("application/json")
50 | }
51 |
52 | }
53 | response {
54 | status OK()
55 | body(
56 | fraudCheckStatus: "OK",
57 | "rejection.reason": $(consumer(null), producer(execute('assertThatRejectionReasonIsNull($it)')))
58 | )
59 | headers {
60 | contentType("application/json")
61 | }
62 | }
63 |
64 | }
65 | }
66 |
67 | static shouldCountAllFrauds(){
68 | return Contract.make {
69 | request {
70 | name "should count all frauds"
71 | method GET()
72 | url '/frauds'
73 | }
74 | response {
75 | status OK()
76 | body([
77 | count: 200
78 | ])
79 | headers {
80 | contentType("application/json")
81 | }
82 | }
83 | }
84 | }
85 |
86 | static shouldCountAllDrunks() {
87 | Contract.make {
88 | request {
89 | method GET()
90 | url '/drunks'
91 | }
92 | response {
93 | status OK()
94 | body([
95 | count: 100
96 | ])
97 | headers {
98 | contentType("application/json")
99 | }
100 | }
101 | }
102 | }
103 |
104 | static shouldReturnACookie(){
105 | Contract.make {
106 | request {
107 | method GET()
108 | url '/frauds/name'
109 | cookies {
110 | cookie("name", "foo")
111 | cookie(name2: "bar")
112 | }
113 | }
114 | response {
115 | status 200
116 | body("foo bar")
117 | }
118 | }
119 | }
120 |
121 | static shouldReturnAFraudForTheName() {
122 | Contract.make {
123 | // highest priority
124 | priority(1)
125 | request {
126 | method PUT()
127 | url '/frauds/name'
128 | body([
129 | name: "fraud"
130 | ])
131 | headers {
132 | contentType("application/json")
133 | }
134 | }
135 | response {
136 | status OK()
137 | body([
138 | result: "Sorry ${fromRequest().body('$.name')} but you're a fraud"
139 | ])
140 | headers {
141 | header(contentType(), "${fromRequest().header(contentType())};charset=UTF-8")
142 | }
143 | }
144 | }
145 | }
146 |
147 | static shouldReturnNonFraudForTheName() {
148 | Contract.make {
149 | request {
150 | method PUT()
151 | url '/frauds/name'
152 | body([
153 | name: $(anyAlphaUnicode())
154 | ])
155 | headers {
156 | contentType("application/json")
157 | }
158 | }
159 | response {
160 | status OK()
161 | body([
162 | result: "Don't worry ${fromRequest().body('$.name')} you're not a fraud"
163 | ])
164 | headers {
165 | header(contentType(), "${fromRequest().header(contentType())};charset=UTF-8")
166 | }
167 | }
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/mvnw.cmd:
--------------------------------------------------------------------------------
1 | @REM ----------------------------------------------------------------------------
2 | @REM Licensed to the Apache Software Foundation (ASF) under one
3 | @REM or more contributor license agreements. See the NOTICE file
4 | @REM distributed with this work for additional information
5 | @REM regarding copyright ownership. The ASF licenses this file
6 | @REM to you under the Apache License, Version 2.0 (the
7 | @REM "License"); you may not use this file except in compliance
8 | @REM with the License. You may obtain a copy of the License at
9 | @REM
10 | @REM http://www.apache.org/licenses/LICENSE-2.0
11 | @REM
12 | @REM Unless required by applicable law or agreed to in writing,
13 | @REM software distributed under the License is distributed on an
14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | @REM KIND, either express or implied. See the License for the
16 | @REM specific language governing permissions and limitations
17 | @REM under the License.
18 | @REM ----------------------------------------------------------------------------
19 |
20 | @REM ----------------------------------------------------------------------------
21 | @REM Maven2 Start Up Batch script
22 | @REM
23 | @REM Required ENV vars:
24 | @REM JAVA_HOME - location of a JDK home dir
25 | @REM
26 | @REM Optional ENV vars
27 | @REM M2_HOME - location of maven2's installed home dir
28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
31 | @REM e.g. to debug Maven itself, use
32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
34 | @REM ----------------------------------------------------------------------------
35 |
36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
37 | @echo off
38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
40 |
41 | @REM set %HOME% to equivalent of $HOME
42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
43 |
44 | @REM Execute a user defined script before this one
45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
49 | :skipRcPre
50 |
51 | @setlocal
52 |
53 | set ERROR_CODE=0
54 |
55 | @REM To isolate internal variables from possible post scripts, we use another setlocal
56 | @setlocal
57 |
58 | @REM ==== START VALIDATION ====
59 | if not "%JAVA_HOME%" == "" goto OkJHome
60 |
61 | echo.
62 | echo Error: JAVA_HOME not found in your environment. >&2
63 | echo Please set the JAVA_HOME variable in your environment to match the >&2
64 | echo location of your Java installation. >&2
65 | echo.
66 | goto error
67 |
68 | :OkJHome
69 | if exist "%JAVA_HOME%\bin\java.exe" goto init
70 |
71 | echo.
72 | echo Error: JAVA_HOME is set to an invalid directory. >&2
73 | echo JAVA_HOME = "%JAVA_HOME%" >&2
74 | echo Please set the JAVA_HOME variable in your environment to match the >&2
75 | echo location of your Java installation. >&2
76 | echo.
77 | goto error
78 |
79 | @REM ==== END VALIDATION ====
80 |
81 | :init
82 |
83 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
84 | @REM Fallback to current working directory if not found.
85 |
86 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
87 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
88 |
89 | set EXEC_DIR=%CD%
90 | set WDIR=%EXEC_DIR%
91 | :findBaseDir
92 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
93 | cd ..
94 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
95 | set WDIR=%CD%
96 | goto findBaseDir
97 |
98 | :baseDirFound
99 | set MAVEN_PROJECTBASEDIR=%WDIR%
100 | cd "%EXEC_DIR%"
101 | goto endDetectBaseDir
102 |
103 | :baseDirNotFound
104 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
105 | cd "%EXEC_DIR%"
106 |
107 | :endDetectBaseDir
108 |
109 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
110 |
111 | @setlocal EnableExtensions EnableDelayedExpansion
112 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
113 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
114 |
115 | :endReadAdditionalConfig
116 |
117 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
118 |
119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
121 |
122 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
123 | if ERRORLEVEL 1 goto error
124 | goto end
125 |
126 | :error
127 | set ERROR_CODE=1
128 |
129 | :end
130 | @endlocal & set ERROR_CODE=%ERROR_CODE%
131 |
132 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
133 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
134 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
135 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
136 | :skipRcPost
137 |
138 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
139 | if "%MAVEN_BATCH_PAUSE%" == "on" pause
140 |
141 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
142 |
143 | exit /B %ERROR_CODE%
144 |
--------------------------------------------------------------------------------
/src/test/resources/openapi/openapi.yml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.0
2 | info:
3 | description: Spring Cloud Contract Verifier Http Server OA3 Sample
4 | version: "1.0.0"
5 | title: Fraud Service API
6 | paths:
7 | /fraudcheck:
8 | put:
9 | summary: Perform Fraud Check
10 | x-contracts:
11 | - contractId: 1
12 | name: Should Mark Client as Fraud
13 | priority: 1
14 | - contractId: 2
15 | name: Should Not Mark Client as Fraud
16 | requestBody:
17 | content:
18 | application/json:
19 | schema:
20 | type: object
21 | properties:
22 | "client.id":
23 | type: integer
24 | loanAmount:
25 | type: integer
26 | x-contracts:
27 | - contractId: 1
28 | body:
29 | "client.id": 1234567890
30 | loanAmount: 99999
31 | matchers:
32 | body:
33 | - path: $.['client.id']
34 | type: by_regex
35 | value: "[0-9]{10}"
36 | - contractId: 2
37 | body:
38 | "client.id": 1234567890
39 | loanAmount: 123.123
40 | matchers:
41 | body:
42 | - path: $.['client.id']
43 | type: by_regex
44 | value: "[0-9]{10}"
45 | responses:
46 | '200':
47 | description: created ok
48 | content:
49 | application/json:
50 | schema:
51 | type: object
52 | properties:
53 | fraudCheckStatus:
54 | type: string
55 | "rejection.reason":
56 | type: string
57 | x-contracts:
58 | - contractId: 1
59 | body:
60 | fraudCheckStatus: "FRAUD"
61 | "rejection.reason": "Amount too high"
62 | headers:
63 | Content-Type: application/json;charset=UTF-8
64 | - contractId: 2
65 | body:
66 | fraudCheckStatus: "OK"
67 | "rejection.reason": null
68 | headers:
69 | Content-Type: application/json;charset=UTF-8
70 | matchers:
71 | body:
72 | - path: $.['rejection.reason']
73 | type: by_command
74 | value: assertThatRejectionReasonIsNull($it)
75 | /frauds:
76 | get:
77 | x-contracts:
78 | - contractId: 3
79 | name: should return all frauds - should count all frauds
80 | responses:
81 | '200':
82 | description: okay
83 | content:
84 | application/json:
85 | schema:
86 | type: object
87 | properties:
88 | count:
89 | type: integer
90 | x-contracts:
91 | - contractId: 3
92 | body:
93 | count: 200
94 | /frauds/name:
95 | put:
96 | x-contracts:
97 | - contractId: 4
98 | name: Should Return a Fraud for the Name
99 | priority: 1
100 | - contractId: 5
101 | name: Should Return Non-Fraud for the Name
102 | requestBody:
103 | content:
104 | application/json:
105 | schema:
106 | type: object
107 | properties:
108 | name:
109 | type: string
110 | x-contracts:
111 | - contractId: 4
112 | body:
113 | name: "fraud"
114 | - contractId: 5
115 | body:
116 | name: "non fraud"
117 | matchers:
118 | body:
119 | - path: $.name
120 | type: by_regex
121 | predefined: only_alpha_unicode
122 | responses:
123 | '200':
124 | description: okay
125 | x-contracts:
126 | - contractId: 4
127 | body:
128 | result: "Sorry {{{ jsonpath this '$.name' }}} but you're a fraud"
129 | headers:
130 | Content-Type: "{{{ request.headers.Content-Type.0 }}}"
131 | - contractId: 5
132 | body:
133 | result: "Don't worry {{{ jsonpath this '$.name' }}} you're not a fraud"
134 | headers:
135 | Content-Type: "{{{ request.headers.Content-Type.0 }}};charset=UTF-8"
136 | /drunks:
137 | get:
138 | x-contracts:
139 | - contractId: 6
140 | name: should return all frauds
141 | responses:
142 | '200':
143 | description: okay
144 | content:
145 | application/json:
146 | schema:
147 | type: object
148 | properties:
149 | count:
150 | type: integer
151 | x-contracts:
152 | - contractId: 6
153 | body:
154 | count: 200
155 |
--------------------------------------------------------------------------------
/src/test/groovy/org/springframework/cloud/contract/verifier/converter/OpenApiContactConverterTest.groovy:
--------------------------------------------------------------------------------
1 | package org.springframework.cloud.contract.verifier.converter
2 |
3 | import org.springframework.cloud.contract.spec.Contract
4 | import spock.lang.Specification
5 |
6 | /**
7 | * Created by jt on 5/24/18.
8 | */
9 | class OpenApiContactConverterTest extends Specification {
10 |
11 | URL contractUrl = OpenApiContactConverterTest.getResource("/yml/contract.yml")
12 | File contractFile = new File(contractUrl.toURI())
13 | URL contractOA3Url = OpenApiContactConverterTest.getResource("/openapi/contract_OA3.yml")
14 | File contractOA3File = new File(contractOA3Url.toURI())
15 |
16 | URL contractOA3UrlPath = OpenApiContactConverterTest.getResource("/openapi/contract_OA3_contractPath.yml")
17 | File contractOA3FilePath = new File(contractOA3UrlPath.toURI())
18 |
19 | URL fruadApiUrl = OpenApiContactConverterTest.getResource("/openapi/openapi.yml")
20 | File fraudApiFile = new File(fruadApiUrl.toURI())
21 |
22 | URL payorApiUrl = OpenApiContactConverterTest.getResource("/openapi/payor_example.yml")
23 | File payorApiFile = new File(payorApiUrl.toURI())
24 |
25 | URL veloApiUrl = OpenApiContactConverterTest.getResource("/openapi/velooa3.yaml")
26 | File veloApiFile = new File(veloApiUrl.toURI())
27 |
28 | URL matcherUrl = OpenApiContactConverterTest.getResource("/yml/contract_matchers.yml")
29 | File matcherFile = new File(matcherUrl.toURI())
30 |
31 | URL matcherUrlOA3 = OpenApiContactConverterTest.getResource("/openapi/contract_matchers.yml")
32 | File matcherFileOA3 = new File(matcherUrlOA3.toURI())
33 |
34 | OpenApiContractConverter contactConverter
35 | YamlContractConverter yamlContractConverter
36 |
37 | void setup() {
38 | contactConverter = new OpenApiContractConverter()
39 | yamlContractConverter = new YamlContractConverter()
40 | }
41 |
42 | def "IsAccepted True"() {
43 | given:
44 | File file = new File('src/test/resources/openapi/openapi_petstore.yml')
45 | when:
46 |
47 | def result = contactConverter.isAccepted(file)
48 |
49 | then:
50 | result
51 | }
52 |
53 | def "IsAccepted True 2"() {
54 | given:
55 | File file = new File('src/test/resources/openapi/openapi.yml')
56 | when:
57 |
58 | def result = contactConverter.isAccepted(file)
59 |
60 | then:
61 | result
62 | }
63 |
64 | def "IsAccepted False"() {
65 | given:
66 | File file = new File('foo')
67 | when:
68 |
69 | def result = contactConverter.isAccepted(file)
70 |
71 | then:
72 | !result
73 |
74 | }
75 |
76 | def "ConvertFrom - should not go boom"() {
77 | given:
78 | File file = new File('src/test/resources/openapi/openapi.yml')
79 | when:
80 |
81 | def result = contactConverter.convertFrom(file)
82 |
83 | println result
84 |
85 | then:
86 | result != null
87 | }
88 |
89 |
90 | def "Test Yaml Contract"() {
91 | given:
92 | Contract yamlContract = yamlContractConverter.convertFrom(contractFile).first()
93 | Collection oa3Contract = contactConverter.convertFrom(contractOA3File)
94 |
95 | when:
96 |
97 | Contract openApiContract = oa3Contract.find { it.name.equalsIgnoreCase("some name") }
98 |
99 | then:
100 | openApiContract
101 | yamlContract.request.url == openApiContract.request.url
102 | yamlContract.request.method == openApiContract.request.method
103 | yamlContract.request.cookies == openApiContract.request.cookies
104 | yamlContract.request.headers == openApiContract.request.headers
105 | yamlContract.request.body == openApiContract.request.body
106 | yamlContract.request.bodyMatchers == openApiContract.request.bodyMatchers
107 | yamlContract.response.status == openApiContract.response.status
108 | yamlContract.response.headers == openApiContract.response.headers
109 | yamlContract.response.bodyMatchers == openApiContract.response.bodyMatchers
110 | yamlContract.response.body == openApiContract.response.body
111 | yamlContract == openApiContract
112 |
113 | }
114 |
115 | def "test OA3 Fraud Yml"() {
116 | given:
117 | Collection oa3Contract = contactConverter.convertFrom(fraudApiFile)
118 |
119 | when:
120 | Contract contract = oa3Contract.getAt(0)
121 |
122 | then:
123 | contract
124 | oa3Contract.size() == 6
125 |
126 | }
127 |
128 | def "Test parse of test path"() {
129 | given:
130 | Collection oa3Contract = contactConverter.convertFrom(contractOA3FilePath)
131 |
132 | when:
133 | Contract contract = oa3Contract.getAt(0)
134 |
135 | then:
136 | contract
137 | contract.getRequest().url.clientValue.equals("/foo1")
138 | }
139 |
140 | def "Test Parse of Payor example contracts"() {
141 |
142 | given:
143 | Collection oa3Contract = contactConverter.convertFrom(payorApiFile)
144 |
145 | when:
146 | Contract contract = oa3Contract.getAt(0)
147 |
148 | then:
149 | contract
150 | }
151 |
152 | def "Test Parse of Velo Contracts"() {
153 |
154 | given:
155 | Collection veloContracts = contactConverter.convertFrom(veloApiFile)
156 |
157 | when:
158 | //Contract contract = oa3Contract.getAt(0)
159 | Contract veloContract = veloContracts.getAt(0)
160 |
161 | then:
162 | //contract
163 | contactConverter.isAccepted(veloApiFile)
164 | }
165 |
166 | def "Test Parse of Matchers"() {
167 |
168 | given:
169 | Contract yamlContract = yamlContractConverter.convertFrom(matcherFile).first()
170 | Collection matcherContracts = contactConverter.convertFrom(matcherFileOA3)
171 |
172 | when:
173 |
174 | Contract openApiContract = matcherContracts.getAt(0)
175 |
176 | then:
177 | //contract
178 | contactConverter.isAccepted(matcherFileOA3)
179 | // yamlContract.request.url == openApiContract.request.url
180 | yamlContract.request.method == openApiContract.request.method
181 | yamlContract.request.cookies == openApiContract.request.cookies
182 | yamlContract.request.headers == openApiContract.request.headers
183 | yamlContract.request.body == openApiContract.request.body // has empty list, which does not convert
184 | yamlContract.request.bodyMatchers == openApiContract.request.bodyMatchers
185 | yamlContract.response.status == openApiContract.response.status
186 | yamlContract.response.headers == openApiContract.response.headers
187 | yamlContract.response.bodyMatchers == openApiContract.response.bodyMatchers
188 | yamlContract.response.body == openApiContract.response.body // has empty list, which does not convert
189 |
190 | }
191 |
192 | }
193 |
--------------------------------------------------------------------------------
/mvnw:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ----------------------------------------------------------------------------
3 | # Licensed to the Apache Software Foundation (ASF) under one
4 | # or more contributor license agreements. See the NOTICE file
5 | # distributed with this work for additional information
6 | # regarding copyright ownership. The ASF licenses this file
7 | # to you under the Apache License, Version 2.0 (the
8 | # "License"); you may not use this file except in compliance
9 | # with the License. You may obtain a copy of the License at
10 | #
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing,
14 | # software distributed under the License is distributed on an
15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | # KIND, either express or implied. See the License for the
17 | # specific language governing permissions and limitations
18 | # under the License.
19 | # ----------------------------------------------------------------------------
20 |
21 | # ----------------------------------------------------------------------------
22 | # Maven2 Start Up Batch script
23 | #
24 | # Required ENV vars:
25 | # ------------------
26 | # JAVA_HOME - location of a JDK home dir
27 | #
28 | # Optional ENV vars
29 | # -----------------
30 | # M2_HOME - location of maven2's installed home dir
31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven
32 | # e.g. to debug Maven itself, use
33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files
35 | # ----------------------------------------------------------------------------
36 |
37 | if [ -z "$MAVEN_SKIP_RC" ] ; then
38 |
39 | if [ -f /etc/mavenrc ] ; then
40 | . /etc/mavenrc
41 | fi
42 |
43 | if [ -f "$HOME/.mavenrc" ] ; then
44 | . "$HOME/.mavenrc"
45 | fi
46 |
47 | fi
48 |
49 | # OS specific support. $var _must_ be set to either true or false.
50 | cygwin=false;
51 | darwin=false;
52 | mingw=false
53 | case "`uname`" in
54 | CYGWIN*) cygwin=true ;;
55 | MINGW*) mingw=true;;
56 | Darwin*) darwin=true
57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
59 | if [ -z "$JAVA_HOME" ]; then
60 | if [ -x "/usr/libexec/java_home" ]; then
61 | export JAVA_HOME="`/usr/libexec/java_home`"
62 | else
63 | export JAVA_HOME="/Library/Java/Home"
64 | fi
65 | fi
66 | ;;
67 | esac
68 |
69 | if [ -z "$JAVA_HOME" ] ; then
70 | if [ -r /etc/gentoo-release ] ; then
71 | JAVA_HOME=`java-config --jre-home`
72 | fi
73 | fi
74 |
75 | if [ -z "$M2_HOME" ] ; then
76 | ## resolve links - $0 may be a link to maven's home
77 | PRG="$0"
78 |
79 | # need this for relative symlinks
80 | while [ -h "$PRG" ] ; do
81 | ls=`ls -ld "$PRG"`
82 | link=`expr "$ls" : '.*-> \(.*\)$'`
83 | if expr "$link" : '/.*' > /dev/null; then
84 | PRG="$link"
85 | else
86 | PRG="`dirname "$PRG"`/$link"
87 | fi
88 | done
89 |
90 | saveddir=`pwd`
91 |
92 | M2_HOME=`dirname "$PRG"`/..
93 |
94 | # make it fully qualified
95 | M2_HOME=`cd "$M2_HOME" && pwd`
96 |
97 | cd "$saveddir"
98 | # echo Using m2 at $M2_HOME
99 | fi
100 |
101 | # For Cygwin, ensure paths are in UNIX format before anything is touched
102 | if $cygwin ; then
103 | [ -n "$M2_HOME" ] &&
104 | M2_HOME=`cygpath --unix "$M2_HOME"`
105 | [ -n "$JAVA_HOME" ] &&
106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
107 | [ -n "$CLASSPATH" ] &&
108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
109 | fi
110 |
111 | # For Migwn, ensure paths are in UNIX format before anything is touched
112 | if $mingw ; then
113 | [ -n "$M2_HOME" ] &&
114 | M2_HOME="`(cd "$M2_HOME"; pwd)`"
115 | [ -n "$JAVA_HOME" ] &&
116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
117 | # TODO classpath?
118 | fi
119 |
120 | if [ -z "$JAVA_HOME" ]; then
121 | javaExecutable="`which javac`"
122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
123 | # readlink(1) is not available as standard on Solaris 10.
124 | readLink=`which readlink`
125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
126 | if $darwin ; then
127 | javaHome="`dirname \"$javaExecutable\"`"
128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
129 | else
130 | javaExecutable="`readlink -f \"$javaExecutable\"`"
131 | fi
132 | javaHome="`dirname \"$javaExecutable\"`"
133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'`
134 | JAVA_HOME="$javaHome"
135 | export JAVA_HOME
136 | fi
137 | fi
138 | fi
139 |
140 | if [ -z "$JAVACMD" ] ; then
141 | if [ -n "$JAVA_HOME" ] ; then
142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
143 | # IBM's JDK on AIX uses strange locations for the executables
144 | JAVACMD="$JAVA_HOME/jre/sh/java"
145 | else
146 | JAVACMD="$JAVA_HOME/bin/java"
147 | fi
148 | else
149 | JAVACMD="`which java`"
150 | fi
151 | fi
152 |
153 | if [ ! -x "$JAVACMD" ] ; then
154 | echo "Error: JAVA_HOME is not defined correctly." >&2
155 | echo " We cannot execute $JAVACMD" >&2
156 | exit 1
157 | fi
158 |
159 | if [ -z "$JAVA_HOME" ] ; then
160 | echo "Warning: JAVA_HOME environment variable is not set."
161 | fi
162 |
163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
164 |
165 | # traverses directory structure from process work directory to filesystem root
166 | # first directory with .mvn subdirectory is considered project base directory
167 | find_maven_basedir() {
168 |
169 | if [ -z "$1" ]
170 | then
171 | echo "Path not specified to find_maven_basedir"
172 | return 1
173 | fi
174 |
175 | basedir="$1"
176 | wdir="$1"
177 | while [ "$wdir" != '/' ] ; do
178 | if [ -d "$wdir"/.mvn ] ; then
179 | basedir=$wdir
180 | break
181 | fi
182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc)
183 | if [ -d "${wdir}" ]; then
184 | wdir=`cd "$wdir/.."; pwd`
185 | fi
186 | # end of workaround
187 | done
188 | echo "${basedir}"
189 | }
190 |
191 | # concatenates all lines of a file
192 | concat_lines() {
193 | if [ -f "$1" ]; then
194 | echo "$(tr -s '\n' ' ' < "$1")"
195 | fi
196 | }
197 |
198 | BASE_DIR=`find_maven_basedir "$(pwd)"`
199 | if [ -z "$BASE_DIR" ]; then
200 | exit 1;
201 | fi
202 |
203 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
204 | echo $MAVEN_PROJECTBASEDIR
205 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
206 |
207 | # For Cygwin, switch paths to Windows format before running java
208 | if $cygwin; then
209 | [ -n "$M2_HOME" ] &&
210 | M2_HOME=`cygpath --path --windows "$M2_HOME"`
211 | [ -n "$JAVA_HOME" ] &&
212 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
213 | [ -n "$CLASSPATH" ] &&
214 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
215 | [ -n "$MAVEN_PROJECTBASEDIR" ] &&
216 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
217 | fi
218 |
219 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
220 |
221 | exec "$JAVACMD" \
222 | $MAVEN_OPTS \
223 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
224 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
225 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
226 |
--------------------------------------------------------------------------------
/src/test/resources/openapi/contract_matchers.yml:
--------------------------------------------------------------------------------
1 | openapi: "3.0.0"
2 | info:
3 | description: Contract Matchers Test
4 | version: "1.0.0"
5 | paths:
6 | /get/1:
7 | get:
8 | x-contracts:
9 | - contractId: 1
10 | name: Contract Matchers Test
11 | requestBody:
12 | content:
13 | application/json:
14 | schema:
15 | type: string
16 | x-contracts:
17 | - contractId: 1
18 | headers:
19 | Content-Type: application/json
20 | cookies:
21 | foo: 2
22 | bar: 3
23 | queryParameters:
24 | limit: 10
25 | offset: 20
26 | filter: 'email'
27 | sort: name
28 | search: 55
29 | age: 99
30 | name: John.Doe
31 | email: 'bob@email.com'
32 | body:
33 | duck: 123
34 | alpha: "abc"
35 | number: 123
36 | aBoolean: true
37 | date: "2017-01-01"
38 | dateTime: "2017-01-01T01:23:45"
39 | time: "01:02:34"
40 | valueWithoutAMatcher: "foo"
41 | valueWithTypeMatch: "string"
42 | key:
43 | "complex.key": 'foo'
44 | nullValue: null
45 | valueWithMin:
46 | - 1
47 | - 2
48 | - 3
49 | valueWithMax:
50 | - 1
51 | - 2
52 | - 3
53 | valueWithMinMax:
54 | - 1
55 | - 2
56 | - 3
57 | valueWithMinEmpty: []
58 | valueWithMaxEmpty: []
59 | matchers:
60 | url:
61 | regex: /get/[0-9]
62 | # predefined:
63 | # execute a method
64 | #command: 'equals($it)'
65 | queryParameters:
66 | - key: limit
67 | type: equal_to
68 | value: 20
69 | - key: offset
70 | type: containing
71 | value: 20
72 | - key: sort
73 | type: equal_to
74 | value: name
75 | - key: search
76 | type: not_matching
77 | value: '^[0-9]{2}$'
78 | - key: age
79 | type: not_matching
80 | value: '^\\w*$'
81 | - key: name
82 | type: matching
83 | value: 'John.*'
84 | - key: hello
85 | type: absent
86 | cookies:
87 | - key: foo
88 | regex: '[0-9]'
89 | - key: bar
90 | command: 'equals($it)'
91 | headers:
92 | - key: Content-Type
93 | regex: "application/json.*"
94 | body:
95 | - path: $.duck
96 | type: by_regex
97 | value: "[0-9]{3}"
98 | - path: $.duck
99 | type: by_equality
100 | - path: $.alpha
101 | type: by_regex
102 | predefined: only_alpha_unicode
103 | - path: $.alpha
104 | type: by_equality
105 | - path: $.number
106 | type: by_regex
107 | predefined: number
108 | - path: $.aBoolean
109 | type: by_regex
110 | predefined: any_boolean
111 | - path: $.date
112 | type: by_date
113 | - path: $.dateTime
114 | type: by_timestamp
115 | - path: $.time
116 | type: by_time
117 | - path: "$.['key'].['complex.key']"
118 | type: by_equality
119 | - path: $.nullvalue
120 | type: by_null
121 | - path: $.valueWithMin
122 | type: by_type
123 | minOccurrence: 1
124 | - path: $.valueWithMax
125 | type: by_type
126 | maxOccurrence: 3
127 | - path: $.valueWithMinMax
128 | type: by_type
129 | minOccurrence: 1
130 | maxOccurrence: 3
131 | responses:
132 | '200':
133 | description: Good repsonse
134 | content:
135 | application/json:
136 | schema:
137 | type: string
138 | x-contracts:
139 | - contractId: 1
140 | cookies:
141 | foo: 1
142 | bar: 2
143 | body:
144 | duck: 123
145 | alpha: "abc"
146 | number: 123
147 | aBoolean: true
148 | date: "2017-01-01"
149 | dateTime: "2017-01-01T01:23:45"
150 | time: "01:02:34"
151 | valueWithoutAMatcher: "foo"
152 | valueWithTypeMatch: "string"
153 | valueWithMin:
154 | - 1
155 | - 2
156 | - 3
157 | valueWithMax:
158 | - 1
159 | - 2
160 | - 3
161 | valueWithMinMax:
162 | - 1
163 | - 2
164 | - 3
165 | valueWithMinEmpty: []
166 | valueWithMaxEmpty: []
167 | key:
168 | 'complex.key': 'foo'
169 | nulValue: null
170 | matchers:
171 | headers:
172 | - key: Content-Type
173 | regex: "application/json.*"
174 | cookies:
175 | - key: foo
176 | regex: '[0-9]'
177 | - key: bar
178 | command: 'equals($it)'
179 | body:
180 | - path: $.duck
181 | type: by_regex
182 | value: "[0-9]{3}"
183 | - path: $.duck
184 | type: by_equality
185 | - path: $.alpha
186 | type: by_regex
187 | predefined: only_alpha_unicode
188 | - path: $.alpha
189 | type: by_equality
190 | - path: $.number
191 | type: by_regex
192 | predefined: number
193 | - path: $.aBoolean
194 | type: by_regex
195 | predefined: any_boolean
196 | - path: $.date
197 | type: by_date
198 | - path: $.dateTime
199 | type: by_timestamp
200 | - path: $.time
201 | type: by_time
202 | - path: $.valueWithTypeMatch
203 | type: by_type
204 | - path: $.valueWithMin
205 | type: by_type
206 | minOccurrence: 1
207 | - path: $.valueWithMax
208 | type: by_type
209 | maxOccurrence: 3
210 | - path: $.valueWithMinMax
211 | type: by_type
212 | minOccurrence: 1
213 | maxOccurrence: 3
214 | - path: $.valueWithMinEmpty
215 | type: by_type
216 | minOccurrence: 0
217 | - path: $.valueWithMaxEmpty
218 | type: by_type
219 | maxOccurrence: 0
220 | - path: $.duck
221 | type: by_command
222 | value: assertThatValueIsANumber($it)
223 | - path: $.nullValue
224 | type: by_null
225 | value: null
226 | headers:
227 | Content-Type: application/json
--------------------------------------------------------------------------------
/src/test/resources/openapi/payor_example.yml:
--------------------------------------------------------------------------------
1 | openapi: "3.0.0"
2 | info:
3 | description: SFG Payor Service
4 | version: "1.0.0"
5 | title: Payor Service
6 | paths:
7 | /v1/payors/{payorId}:
8 | get:
9 | summary: Get Payor Details by ID
10 | description: |
11 | This API operation provide you the account balance of the given payor id.
12 | operationId: getPayor
13 | tags:
14 | - Payor Service
15 | parameters:
16 | - name: payorId
17 | in: path
18 | description: The account owner Payor ID
19 | required: true
20 | schema:
21 | type: string
22 | format: uuid
23 | x-code-samples:
24 | - lang: 'shell'
25 | source: |
26 | $ curl 'https://api.sandbox.velopayments.com/v1/payors/a3c35f60-c94e-418d-bb5b-00174d93a6eb' -i \
27 | -H 'Content-Type: application/json' \
28 | -H 'Authorization: Bearer 40a61fcb-2a1a-454b-b049-01487986873e'
29 | x-contracts:
30 | - contractId: 1
31 | name: Test Good Response
32 | contractPath: "/v1/payors/0a818933-087d-47f2-ad83-2f986ed087eb"
33 | - contractId: 2
34 | name: Test Not Found
35 | contractPath: "/v1/payors/00000000-0000-0000-0000-000000000000"
36 | - contractId: 3
37 | name: Test Bad Schema
38 | contractPath: "/v1/payors/16ea5c75-1476-4651-b117-bcb33d77f2b5"
39 | responses:
40 | '200':
41 | description: Get Payor Details
42 | content:
43 | application/json:
44 | schema:
45 | type: object
46 | required:
47 | - payorId
48 | - payorName
49 | - address
50 | - primaryContactName
51 | - primaryContactPhone
52 | - primaryContactEmail
53 | - language
54 | properties:
55 | payorId:
56 | type: string
57 | format: uuid
58 | payorName:
59 | type: string
60 | address:
61 | type: object
62 | required:
63 | - line1
64 | - city
65 | - zipOrPostcode
66 | - country
67 | properties:
68 | line1:
69 | type: string
70 | minLength: 2
71 | maxLength: 255
72 | line2:
73 | type: string
74 | minLength: 0
75 | maxLength: 255
76 | line3:
77 | type: string
78 | minLength: 0
79 | maxLength: 255
80 | line4:
81 | type: string
82 | minLength: 0
83 | maxLength: 255
84 | city:
85 | type: string
86 | minLength: 2
87 | maxLength: 100
88 | countyOrProvince:
89 | type: string
90 | minLength: 2
91 | maxLength: 100
92 | zipOrPostcode:
93 | type: string
94 | minLength: 2
95 | maxLength: 30
96 | country:
97 | type: string
98 | minLength: 2
99 | maxLength: 50
100 | primaryContactName:
101 | type: string
102 | description: Name of primary contact for the payor.
103 | primaryContactPhone:
104 | type: string
105 | description: Primary contact phone number for the payor.
106 | primaryContactEmail:
107 | type: string
108 | format: email
109 | description: Primary contact email for the payor.
110 | fundingAccountRoutingNumber:
111 | type: string
112 | description: The funding account routing number to be used for the payor.
113 | fundingAccountAccountNumber:
114 | type: string
115 | description: The funding account number to be used for the payor.
116 | fundingAccountAccountName:
117 | type: string
118 | description: The funding account name to be used for the payor.
119 | kycState:
120 | type: string
121 | description: The kyc state of the payor.
122 | enum: [FAILED_KYC, PASSED_KYC, REQUIRES_KYC]
123 | manualLockout:
124 | type: boolean
125 | description: Whether or not the payor has been manually locked by the backoffice.
126 | payeeGracePeriodProcessingEnabled:
127 | type: boolean
128 | description: Whether grace period processing is enabled.
129 | payeeGracePeriodDays:
130 | type: integer
131 | description: The grace period for paying payees in days.
132 | collectiveAlias:
133 | type: string
134 | description: How the payor has chosen to refer to payees.
135 | supportContact:
136 | type: string
137 | description: The payor’s support contact address.
138 | dbaName:
139 | type: string
140 | description: The payor’s 'Doing Business As' name.
141 | allowsLanguageChoice:
142 | type: boolean
143 | description: Whether or not the payor allows language choice in the UI.
144 | reminderEmailsOptOut:
145 | type: boolean
146 | description: Whether or not the payor has opted-out of reminder emails being sent.
147 | language:
148 | type: string
149 | description: The payor’s language preference. Must be one of [EN, FR].
150 | enum: [EN, FR]
151 | includesReports:
152 | type: boolean
153 | x-contracts:
154 | - contractId: 1
155 | body:
156 | payorId: "0a818933-087d-47f2-ad83-2f986ed087eb"
157 | '404':
158 | description: Payor Id Not Found
159 | x-contracts:
160 | - contractId: 2
161 | '500':
162 | description: Server Error
163 | x-contracts:
164 | - contractId: 3
165 | body:
166 | payorId: "0a818933-087d-47f2-ad83-2f986ed087eb"
167 | /v1/payors/{payorId}/applications:
168 | post:
169 | summary: Create application
170 | operationId: createPayorApplication
171 | tags:
172 | - Payor Service
173 | parameters:
174 | - name: Content-Type
175 | in: header
176 | description: Content Type
177 | required: true
178 | schema:
179 | type: string
180 | - name: payorId
181 | in: path
182 | description: The account owner Payor ID
183 | required: true
184 | schema:
185 | type: string
186 | format: uuid
187 | x-contracts:
188 | - contractId: 1
189 | name: Test Create Application
190 | contractPath: "/v1/payors/835ac62e-616c-4261-bf38-63366db78c0d/applications"
191 | requestBody:
192 | content:
193 | application/json:
194 | schema:
195 | type: object
196 | properties:
197 | name:
198 | type: string
199 | description:
200 | type: string
201 | x-contracts:
202 | - contractId: 1
203 | body:
204 | name: 'foo'
205 | description: "a foo application"
206 | responses:
207 | '201':
208 | description: Success
209 | headers:
210 | 'Location':
211 | description: location
212 | schema:
213 | type: string
214 | x-contracts:
215 | - contractId: 1
216 | mathers:
217 | headers:
218 | - key: Location
219 | predefined: non_empty
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
203 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 | guru.springframework
6 | spring-cloud-contract-oa3
7 | 2.1.2.1-SNAPSHOT
8 | jar
9 |
10 | spring-cloud-contract-oa3
11 | Spring Cloud Contract OpenAPI 3.0
12 |
13 | https://github.com/springframeworkguru/spring-cloud-contract-oa3
14 |
15 |
16 | Spring Framework Guru
17 | https://springframework.guru/
18 |
19 |
20 |
21 |
22 | jt
23 | John Thompson
24 | john@springframework.guru
25 |
26 |
27 |
28 | 2018
29 |
30 |
31 |
32 | The Apache License, Version 2.0
33 | http://www.apache.org/licenses/LICENSE-2.0.txt
34 |
35 |
36 |
37 |
38 | UTF-8
39 | UTF-8
40 | 1.8
41 | ${java.version}
42 | ${java.version}
43 | src/main/java,src/main/groovy
44 | src/test/java,src/test/groovy
45 | 2.1.2.RELEASE
46 | 1.0-groovy-2.4
47 | 0.5.1
48 | 2.5.8
49 |
50 |
51 |
52 |
53 | org.springframework.cloud
54 | spring-cloud-contract-verifier
55 | ${spring.cloud.contract.version}
56 |
57 |
58 | io.swagger.parser.v3
59 | swagger-parser
60 | 2.0.13
61 |
62 |
63 | org.spockframework
64 | spock-spring
65 | ${spock-spring.version}
66 | test
67 |
68 |
69 | org.codehaus.groovy
70 | groovy-all
71 |
72 |
73 | org.codehaus.groovy
74 | groovy
75 |
76 |
77 |
78 |
79 | org.spockframework
80 | spock-core
81 | ${spock-spring.version}
82 | test
83 |
84 |
85 | org.codehaus.groovy
86 | groovy-all
87 |
88 |
89 | org.codehaus.groovy
90 | groovy
91 |
92 |
93 |
94 |
95 | info.solidsoft.spock
96 | spock-global-unroll
97 | ${spock-global-unroll.version}
98 | test
99 |
100 |
101 |
102 |
103 | [3.2.1,)
104 |
105 |
106 |
107 | scm:git:git@github.com:springframeworkguru/spring-cloud-contract-oa3.git
108 | scm:git:git@github.com:springframeworkguru/spring-cloud-contract-oa3.git
109 |
110 | https://github.com/springframeworkguru/spring-cloud-contract-oa3
111 | HEAD
112 |
113 |
114 |
115 | GitHub
116 | https://github.com/springframeworkguru/spring-cloud-contract-oa3/issues
117 |
118 |
119 |
120 | CircleCi
121 | https://circleci.com/gh/springframeworkguru/spring-cloud-contract-oa3
122 |
123 |
124 |
125 |
126 |
127 |
128 | org.codehaus.gmavenplus
129 | gmavenplus-plugin
130 | 1.6.1
131 |
132 |
133 |
134 |
135 |
136 | org.codehaus.gmavenplus
137 | gmavenplus-plugin
138 |
139 |
140 |
141 | addSources
142 | addTestSources
143 | generateStubs
144 | compile
145 | generateTestStubs
146 | compileTests
147 | removeStubs
148 | removeTestStubs
149 |
150 |
151 |
152 |
153 |
154 | org.sonatype.plugins
155 | nexus-staging-maven-plugin
156 | 1.6.7
157 | true
158 |
159 | ossrh
160 | https://oss.sonatype.org/
161 | true
162 |
163 |
164 |
165 | org.apache.maven.plugins
166 | maven-source-plugin
167 | 3.0.1
168 |
169 |
170 | attach-sources
171 |
172 | jar-no-fork
173 |
174 |
175 |
176 |
177 |
178 | org.apache.maven.plugins
179 | maven-javadoc-plugin
180 | 3.0.1
181 |
182 |
183 | attach-javadocs
184 |
185 | jar
186 |
187 |
188 |
189 |
190 |
191 | org.apache.maven.plugins
192 | maven-gpg-plugin
193 | 1.6
194 |
195 |
196 | sign-artifacts
197 | verify
198 |
199 | sign
200 |
201 |
202 |
203 |
204 |
205 | org.apache.maven.plugins
206 | maven-release-plugin
207 | 2.5.3
208 |
209 | true
210 | false
211 | release
212 | deploy
213 |
214 |
215 |
216 | org.sonarsource.scanner.maven
217 | sonar-maven-plugin
218 | 3.5.0.1254
219 |
220 |
221 | org.apache.maven.plugins
222 | maven-site-plugin
223 | 3.7.1
224 |
225 |
226 | org.codehaus.mojo
227 | flatten-maven-plugin
228 |
229 | ossrh
230 |
231 |
232 |
233 |
234 | flatten
235 | process-resources
236 |
237 | flatten
238 |
239 |
240 |
241 |
242 | flatten.clean
243 | clean
244 |
245 | clean
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 | ossrh
256 | https://oss.sonatype.org/content/repositories/snapshots
257 |
258 |
259 | ossrh
260 | https://oss.sonatype.org/service/local/staging/deploy/maven2/
261 |
262 |
263 |
264 |
265 |
266 | spring-snapshots
267 | Spring Snapshots
268 | https://repo.spring.io/snapshot
269 |
270 | true
271 |
272 |
273 |
274 | spring-milestones
275 | Spring Milestones
276 | https://repo.spring.io/milestone
277 |
278 | false
279 |
280 |
281 |
282 | spring-releases
283 | Spring Releases
284 | https://repo.spring.io/release
285 |
286 | false
287 |
288 |
289 |
290 |
291 |
292 | spring-snapshots
293 | Spring Snapshots
294 | https://repo.spring.io/snapshot
295 |
296 | true
297 |
298 |
299 |
300 | spring-milestones
301 | Spring Milestones
302 | https://repo.spring.io/milestone
303 |
304 | false
305 |
306 |
307 |
308 | spring-releases
309 | Spring Releases
310 | https://repo.spring.io/release
311 |
312 | false
313 |
314 |
315 |
316 |
317 |
--------------------------------------------------------------------------------
/src/main/groovy/org/springframework/cloud/contract/verifier/converter/OpenApiContractConverter.groovy:
--------------------------------------------------------------------------------
1 | package org.springframework.cloud.contract.verifier.converter
2 |
3 | import com.fasterxml.jackson.annotation.JsonInclude
4 | import com.fasterxml.jackson.databind.ObjectMapper
5 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
6 | import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator
7 | import groovy.util.logging.Slf4j
8 | import io.swagger.v3.oas.models.PathItem
9 | import io.swagger.v3.parser.OpenAPIV3Parser
10 | import org.apache.commons.lang3.StringUtils
11 | import org.springframework.beans.factory.annotation.Autowired
12 | import org.springframework.cloud.contract.spec.Contract
13 | import org.springframework.cloud.contract.spec.ContractConverter
14 | import org.springframework.context.ApplicationContext
15 |
16 |
17 | /**
18 | * Created by John Thompson on 5/24/18.
19 | */
20 | @Slf4j
21 | class OpenApiContractConverter implements ContractConverter> {
22 |
23 | public static final OpenApiContractConverter INSTANCE = new OpenApiContractConverter()
24 | public static final String SERVICE_NAME_KEY = "scc.enabled.servicenames"
25 |
26 | private YamlToContracts yamlToContracts = new YamlToContracts()
27 |
28 | @Autowired
29 | private ApplicationContext appContext
30 |
31 | @Override
32 | boolean isAccepted(File file) {
33 |
34 | try {
35 | def spec = new OpenAPIV3Parser().read(file.path)
36 |
37 | if (spec == null) {
38 | log.debug("Spec Not Found")
39 | throw new RuntimeException("Spec not found")
40 | }
41 |
42 | if (spec.paths.size() == 0) { // could toss NPE, which is also invalid spec
43 | log.debug("No Paths Found")
44 | throw new RuntimeException("No paths found")
45 | }
46 |
47 | def contractsFound = false
48 | //check spec for contracts
49 | spec.paths.each { k, v ->
50 | if (!contractsFound) {
51 | v.readOperations().each { operation ->
52 | if (operation.extensions) {
53 | def contracts = operation.extensions."x-contracts"
54 | if (contracts != null && contracts.size > 0) {
55 | contractsFound = true
56 | }
57 | }
58 | }
59 | }
60 | }
61 |
62 | return contractsFound
63 | } catch (Exception e) {
64 | log.error("Unexpected error in reading contract file")
65 | log.error(e.message)
66 | return false
67 | }
68 | }
69 |
70 | @Override
71 | Collection convertFrom(File file) {
72 | Collection sccContracts = []
73 |
74 | def spec = new OpenAPIV3Parser().read(file.path)
75 |
76 | spec?.paths?.each { path, pathItem ->
77 | pathItem.readOperations().each { operation ->
78 | if (operation?.extensions?."x-contracts") {
79 | operation.extensions."x-contracts".each { openApiContract ->
80 |
81 | if(checkServiceEnabled(openApiContract.serviceName)) {
82 |
83 | YamlContract yamlContract = new YamlContract()
84 | yamlContract.name = openApiContract?.name
85 | yamlContract.description = openApiContract?.description
86 | yamlContract.priority = openApiContract?.priority
87 | yamlContract.label = openApiContract?.label
88 |
89 | def contractId = openApiContract.contractId
90 |
91 | if (openApiContract.ignored != null) {
92 | yamlContract.ignored = openApiContract.ignored
93 | } else {
94 | yamlContract.ignored = false
95 | }
96 |
97 | def contractPath = (StringUtils.isEmpty(openApiContract.contractPath)) ? path : openApiContract.contractPath
98 |
99 | yamlContract.request = new YamlContract.Request()
100 | yamlContract.request.url = contractPath
101 |
102 | if (pathItem?.get?.is(operation)) {
103 | yamlContract.request.method = "GET"
104 | } else if (pathItem?.put?.is(operation)) {
105 | yamlContract.request?.method = "PUT"
106 | } else if (pathItem?.post?.is(operation)) {
107 | yamlContract.request.method = "POST"
108 | } else if (pathItem?.delete?.is(operation)) {
109 | yamlContract.request?.method = "DELETE"
110 | } else if (pathItem?.patch?.is(operation)) {
111 | yamlContract?.request?.method = "PATCH"
112 | }
113 |
114 | if (openApiContract?.request?.queryParameters) {
115 | openApiContract?.request?.queryParameters.each { queryParameter ->
116 | yamlContract.request.queryParameters.put(queryParameter.key, queryParameter.value)
117 |
118 | if (queryParameter.matchers) {
119 | queryParameter?.matchers?.each { contractMatcher ->
120 | YamlContract.QueryParameterMatcher queryParameterMatcher = new YamlContract.QueryParameterMatcher()
121 | queryParameterMatcher.key = queryParameter.name
122 | queryParameterMatcher.value = contractMatcher.value
123 | queryParameterMatcher.type = getMatchingTypeFromString(contractMatcher.type)
124 | yamlContract.request.matchers.queryParameters.add(queryParameterMatcher)
125 | }
126 | }
127 | }
128 | }
129 |
130 | operation?.parameters?.each { openApiParam ->
131 | openApiParam?.extensions?."x-contracts".each { contractParam ->
132 | if (contractParam.contractId == contractId) {
133 | if (openApiParam.in == 'path' || openApiParam.in == 'query') {
134 | yamlContract.request.queryParameters.put(openApiParam.name, contractParam.value)
135 |
136 | if (contractParam.matchers) {
137 | contractParam?.matchers?.each { contractMatcher ->
138 | YamlContract.QueryParameterMatcher queryParameterMatcher = new YamlContract.QueryParameterMatcher()
139 | queryParameterMatcher.key = openApiParam.name
140 | queryParameterMatcher.value = contractMatcher.value
141 | queryParameterMatcher.type = getMatchingTypeFromString(contractMatcher.type)
142 | yamlContract.request.matchers.queryParameters.add(queryParameterMatcher)
143 | }
144 | }
145 | }
146 | if (openApiParam.in == 'header') {
147 | yamlContract.request.headers.put(openApiParam.name, contractParam.value)
148 |
149 | if (contractParam.matchers) {
150 | contractParam?.matchers?.each { headerMatcher ->
151 | YamlContract.HeadersMatcher headersMatcher = new YamlContract.HeadersMatcher()
152 | headersMatcher.key = openApiParam.name
153 | headersMatcher.regex = headerMatcher.regex
154 | headersMatcher.predefined = getPredefinedRegexFromString(headerMatcher.predefined)
155 | headersMatcher.command = headerMatcher.command
156 | headersMatcher.regexType = getRegexTypeFromString(headerMatcher.regexType)
157 | yamlContract.request.matchers.headers.add(headerMatcher)
158 | }
159 | }
160 | }
161 |
162 | if (openApiParam.in == 'cookie') {
163 | yamlContract.request.cookies.put(openApiParam.name, contractParam.value)
164 | }
165 | }
166 | }
167 | }
168 |
169 | if (operation?.requestBody?.extensions?."x-contracts") {
170 | operation?.requestBody?.extensions?."x-contracts"?.each { contractBody ->
171 | if (contractBody?.contractId == contractId) {
172 |
173 | contractBody?.headers?.each { k, v ->
174 | yamlContract.request.headers.put(k, v)
175 | }
176 |
177 | if (contractBody?.cookies) {
178 | contractBody?.cookies?.each { cookieVal ->
179 | yamlContract.request.cookies.put(cookieVal.key, cookieVal.value)
180 | }
181 | }
182 |
183 | yamlContract.request.body = contractBody?.body
184 | yamlContract.request.bodyFromFile = contractBody?.bodyFromFile
185 | yamlContract.request.bodyFromFileAsBytes = contractBody?.bodyFromFileAsBytes
186 |
187 | if (contractBody.multipart) {
188 | yamlContract.request.multipart = new YamlContract.Multipart()
189 | yamlContract.request.matchers.multipart = new YamlContract.MultipartStubMatcher()
190 |
191 | contractBody.multipart?.params?.each { val ->
192 | try {
193 | yamlContract.request.multipart.params.put(val.key, val.value)
194 | } catch (Exception e) {
195 | log.error("Error processing multipart params", e)
196 | }
197 | }
198 |
199 | contractBody.multipart?.named.each { contractNamed ->
200 | YamlContract.Named named = new YamlContract.Named()
201 | named.fileContent = contractNamed.fileContent
202 | named.fileName = contractNamed.fileName
203 | named.paramName = contractNamed.paramName
204 | yamlContract.request.multipart.named.add(named)
205 | }
206 | }
207 |
208 | if (contractBody.matchers?.url) {
209 | YamlContract.KeyValueMatcher keyValueMatcher = new YamlContract.KeyValueMatcher()
210 | keyValueMatcher.key = contractBody.matchers?.url?.key
211 | keyValueMatcher.regex = contractBody.matchers?.url?.regex
212 | keyValueMatcher.predefined = contractBody.matchers?.url?.predefined
213 | keyValueMatcher.command = contractBody.matchers?.url?.command
214 | keyValueMatcher.regexType = contractBody.matchers?.url?.regexType
215 |
216 | yamlContract.request.matchers.url = keyValueMatcher
217 | }
218 |
219 | contractBody.matchers?.queryParameters?.each { matcher ->
220 | YamlContract.QueryParameterMatcher queryParameterMatcher = new YamlContract.QueryParameterMatcher()
221 | queryParameterMatcher.key = matcher.key
222 | queryParameterMatcher.value = matcher.value
223 | queryParameterMatcher.type = getMatchingTypeFromString(matcher?.type)
224 |
225 | yamlContract.request.matchers.queryParameters.add(queryParameterMatcher)
226 | }
227 |
228 | contractBody.matchers?.body?.each { bodyMatcher ->
229 | YamlContract.BodyStubMatcher bodyStubMatcher = new YamlContract.BodyStubMatcher()
230 | bodyStubMatcher.path = bodyMatcher?.path
231 | bodyStubMatcher.value = bodyMatcher.value
232 |
233 | try {
234 | if (StringUtils.isNotEmpty(bodyMatcher.type)) {
235 | bodyStubMatcher.type = YamlContract.StubMatcherType.valueOf(bodyMatcher.type)
236 | }
237 |
238 | bodyStubMatcher.predefined = getPredefinedRegexFromString(bodyMatcher.predefined)
239 | bodyStubMatcher.minOccurrence = bodyMatcher?.minOccurrence
240 | bodyStubMatcher.maxOccurrence = bodyMatcher?.maxOccurrence
241 | bodyStubMatcher.regexType = getRegexTypeFromString(bodyMatcher.regexType)
242 |
243 | } catch (Exception e) {
244 | log.error("Error parsing body matcher in request", e)
245 | }
246 |
247 | yamlContract.request.matchers.body.add(bodyStubMatcher)
248 | }
249 |
250 | contractBody.matchers?.headers?.each { matcher ->
251 | yamlContract.request.matchers.headers.add(buildKeyValueMatcher(matcher))
252 | }
253 |
254 | contractBody.matchers?.cookies?.each { matcher ->
255 | yamlContract.request.matchers.cookies.add(buildKeyValueMatcher(matcher))
256 | }
257 |
258 | contractBody.matchers?.multipart?.each { matcher ->
259 | matcher.params?.each { param ->
260 | yamlContract.request.matchers.multipart.params.add(buildKeyValueMatcher(param))
261 | }
262 |
263 | matcher?.named?.each { namedParam ->
264 | YamlContract.MultipartNamedStubMatcher stubMatcher = new YamlContract.MultipartNamedStubMatcher()
265 | stubMatcher.paramName = namedParam.paramName
266 | try {
267 |
268 | if (StringUtils.isNotEmpty(namedParam?.fileName?.reqex)) {
269 | stubMatcher.fileName = new YamlContract.ValueMatcher(
270 | regex: namedParam?.fileName?.reqex,
271 | predefined: getPredefinedRegexFromString(matcher?.fileName?.predefined))
272 | }
273 |
274 | if (StringUtils.isNotEmpty(namedParam?.fileContent?.reqex)) {
275 | stubMatcher.fileContent = new YamlContract.ValueMatcher(
276 | regex: namedParam?.fileContent?.reqex,
277 | predefined: getPredefinedRegexFromString(matcher?.fileContent?.predefined))
278 | }
279 |
280 | if (StringUtils.isNotEmpty(namedParam?.contentType?.reqex)) {
281 | stubMatcher.contentType = new YamlContract.ValueMatcher(
282 | regex: namedParam?.contentType?.reqex,
283 | predefined: getPredefinedRegexFromString(matcher?.contentType?.predefined))
284 | }
285 | } catch (Exception e) {
286 | log.error("Error parsging multipart matcher in request", e)
287 | }
288 |
289 | yamlContract.request.matchers.multipart.named.add(stubMatcher)
290 | }
291 | }
292 | } // contract request
293 | }
294 | }
295 |
296 | // process responses
297 | if (operation?.responses) {
298 | operation.responses.each { openApiResponse ->
299 | if (openApiResponse?.value?.extensions?.'x-contracts') {
300 | openApiResponse?.value?.extensions?.'x-contracts'?.each { responseContract ->
301 | if (responseContract.contractId == contractId) {
302 | yamlContract.response = new YamlContract.Response()
303 |
304 | def httpResponse = openApiResponse.key.replaceAll("[^a-zA-Z0-9 ]+", "")
305 |
306 | yamlContract.response.status = Integer.valueOf(httpResponse)
307 |
308 | yamlContract.response.body = responseContract?.body
309 | yamlContract.response.bodyFromFile = responseContract?.bodyFromFile
310 | yamlContract.response.bodyFromFileAsBytes = responseContract?.bodyFromFileAsBytes
311 |
312 | responseContract.headers?.each { responseHeader ->
313 | yamlContract.response.headers.put(responseHeader.key, responseHeader.value)
314 | }
315 | //matchers
316 | responseContract?.matchers?.body?.each { matcher ->
317 | YamlContract.BodyTestMatcher bodyTestMatcher = new YamlContract.BodyTestMatcher()
318 | bodyTestMatcher.path = matcher.path
319 | bodyTestMatcher.value = matcher.value
320 |
321 | try {
322 | if (StringUtils.isNotEmpty(matcher.type)) {
323 | bodyTestMatcher.type = YamlContract.TestMatcherType.valueOf(matcher.type)
324 | }
325 | bodyTestMatcher.minOccurrence = matcher?.minOccurrence
326 | bodyTestMatcher.maxOccurrence = matcher?.maxOccurrence
327 | bodyTestMatcher.predefined = getPredefinedRegexFromString(matcher.predefined)
328 | bodyTestMatcher.regexType = getRegexTypeFromString(matcher.regexType)
329 |
330 | } catch (Exception e) {
331 | log.error("Error parsing body matcher in response", e)
332 | }
333 |
334 | yamlContract.response.matchers.body.add(bodyTestMatcher)
335 | }
336 |
337 | responseContract?.matchers?.headers?.each { headerMatcher ->
338 | YamlContract.TestHeaderMatcher testHeaderMatcher = new YamlContract.TestHeaderMatcher()
339 | testHeaderMatcher.key = headerMatcher.key
340 | testHeaderMatcher.regex = headerMatcher.regex
341 | testHeaderMatcher.command = headerMatcher.command
342 | testHeaderMatcher.predefined = getPredefinedRegexFromString(headerMatcher.predefined)
343 | testHeaderMatcher.regexType = getRegexTypeFromString(headerMatcher.regexType)
344 |
345 | yamlContract.response.matchers.headers.add(testHeaderMatcher)
346 | }
347 |
348 | responseContract?.matchers?.cookies?.each { matcher ->
349 | YamlContract.TestCookieMatcher testCookieMatcher = new YamlContract.TestCookieMatcher()
350 | testCookieMatcher.key = matcher.key
351 | testCookieMatcher.regex = matcher.regex
352 | testCookieMatcher.command = matcher.command
353 | testCookieMatcher.predefined = getPredefinedRegexFromString(matcher.predefined)
354 | testCookieMatcher.regexType = getRegexTypeFromString(matcher.regexType)
355 |
356 | yamlContract.response.matchers.cookies.add(testCookieMatcher)
357 | }
358 |
359 | try {
360 | yamlContract.response.async = responseContract.async
361 |
362 | if (StringUtils.isNotEmpty(responseContract.fixedDelayMilliseconds)
363 | && StringUtils.isNumeric(responseContract.fixedDelayMilliseconds)) {
364 | yamlContract.response.fixedDelayMilliseconds = responseContract.fixedDelayMilliseconds
365 | }
366 | } catch (Exception e) {
367 | log.error("Error with setting aysnc property or fixedDelayMilliseconds", e)
368 | }
369 | }
370 | }
371 | }
372 | }
373 | }
374 |
375 | if (!yamlContract.response) {
376 | println "Warning: Response Object is null. Verify Response Object on contract and for proper contract Ids"
377 | yamlContract.response = new YamlContract.Response()
378 | // prevents NPE in contract conversion
379 | }
380 |
381 | File tempFile = File.createTempFile("sccoa3", ".yml")
382 |
383 | ObjectMapper mapper = new ObjectMapper(new YAMLFactory().disable(YAMLGenerator.Feature.SPLIT_LINES).enable(YAMLGenerator.Feature.LITERAL_BLOCK_STYLE))
384 | mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS)
385 | mapper.writeValue(tempFile, yamlContract)
386 |
387 | log.debug(tempFile.getAbsolutePath())
388 |
389 | sccContracts.addAll(yamlToContracts.convertFrom(tempFile))
390 | } else {
391 | YamlContract ignored = new YamlContract()
392 | ignored.name = "Ignored Contract"
393 | ignored.ignored = true
394 | ignored.request = new YamlContract.Request()
395 | ignored.request.url = "/ignored"
396 | ignored.request.method = "GET"
397 | ignored.response = new YamlContract.Response()
398 | // sccContracts.add(ignored)
399 | }
400 | }
401 | }
402 | }
403 | }
404 |
405 | return sccContracts
406 | }
407 |
408 | YamlContract.KeyValueMatcher buildKeyValueMatcher(def matcher) {
409 | YamlContract.KeyValueMatcher keyValueMatcher = new YamlContract.KeyValueMatcher()
410 | keyValueMatcher.key = matcher?.key
411 | keyValueMatcher.regex = matcher?.regex
412 | keyValueMatcher.command = matcher?.command
413 | keyValueMatcher.predefined = getPredefinedRegexFromString(matcher?.predefined)
414 | keyValueMatcher.regexType = getRegexTypeFromString(matcher?.regexType)
415 |
416 | return keyValueMatcher
417 | }
418 |
419 | YamlContract.PredefinedRegex getPredefinedRegexFromString(String val){
420 | if (StringUtils.isNotBlank(val)) {
421 | try {
422 | return YamlContract.PredefinedRegex.valueOf(val)
423 | } catch (Exception e) {
424 | log.error("Error parsing PredefinedRegex", e)
425 | }
426 | }
427 | return null
428 | }
429 |
430 | YamlContract.RegexType getRegexTypeFromString(String val) {
431 | if (StringUtils.isNotBlank(val)) {
432 | try {
433 | return YamlContract.RegexType.valueOf(val)
434 | } catch (Exception e) {
435 | log.error("Error parsing RegexType", e)
436 | }
437 | }
438 | return null
439 | }
440 |
441 | YamlContract.MatchingType getMatchingTypeFromString(String val) {
442 | if (StringUtils.isNotBlank(val)) {
443 | try {
444 | return YamlContract.MatchingType.valueOf(val)
445 | } catch (Exception e) {
446 | log.error("Error parsing MatchingType", e)
447 | }
448 | }
449 | return null
450 | }
451 |
452 | @Override
453 | Collection convertTo(Collection contract) {
454 | throw new RuntimeException("Not Implemented")
455 | }
456 |
457 | boolean checkServiceEnabled(String serviceName){
458 |
459 | //if not set on contract or sys env, return true
460 | if(!serviceName) {
461 | log.debug("Service Name Not Set on Contract, returning true")
462 | return true
463 | }
464 |
465 | String[] propValues = StringUtils.split(System.getProperty(SERVICE_NAME_KEY), ',')?.collect {StringUtils.trim(it)}
466 |
467 | if (!propValues) {
468 | log.debug("System Property - " + SERVICE_NAME_KEY + " - Not set, returning true ")
469 | return true
470 | }
471 |
472 | return propValues.contains(serviceName)
473 |
474 | }
475 | }
476 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Spring Cloud Contract OpenAPI 3.0 Contract Converter
2 |
3 | [](https://gitter.im/spring-cloud-contract-oa3/Lobby)
4 |
5 | [](https://circleci.com/gh/springframeworkguru/spring-cloud-contract-oa3)
6 |
7 | 
8 | ## OpenAPI 3.0 Converter
9 |
10 | The [OpenAPI Specification (OAS)](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md) defines a
11 | standard, language-agnostic interface to RESTful APIs which allows both humans and computers to discover and understand
12 | the capabilities of the service without access to source code, documentation, or through network traffic inspection.
13 | When properly defined, a consumer can understand and interact with the remote service with a minimal amount of
14 | implementation logic.
15 |
16 | An OpenAPI definition can then be used by documentation generation tools to display the API, code generation tools to
17 | generate servers and clients in various programming languages, testing tools, and many other use cases.
18 |
19 | ## Example Project
20 | A complete working example project using Open API 3.0 to define contracts for Spring Cloud Contract is available
21 | [here on GitHub](https://github.com/springframeworkguru/sccoa3-fraud-example).
22 |
23 | This project is a copy of the fraud API example commonly used in the standalone examples. The above example implements
24 | the same producer, client, and contracts (defined in YAML) from the [standalone YAML example](https://github.com/springframeworkguru/sccoa3-fraud-example).
25 |
26 | ## Usage
27 | ### Maven
28 | To enable this plugin, you will need to add the OA3 converter jar to your Spring Boot project as follows.
29 |
30 | 1. Configure your project to use [Spring Cloud Contract](https://cloud.spring.io/spring-cloud-static/spring-cloud-contract/2.1.0.RELEASE/single/spring-cloud-contract.html#maven-project).
31 |
32 | 2. Add to your maven dependencies:
33 | ```xml
34 |
35 | org.springframework.cloud
36 | spring-cloud-starter-contract-verifier
37 | test
38 |
39 |
40 | org.springframework.boot
41 | spring-boot-starter-test
42 | test
43 |
44 | ```
45 | 3. The artifact also needs to be added to the Maven Plugin:
46 |
47 | ```xml
48 |
49 | org.springframework.cloud
50 | spring-cloud-contract-maven-plugin
51 | ${spring-cloud-contract.version}
52 | true
53 |
54 | com.example.fraud
55 |
56 |
57 |
58 |
59 | guru.springframework
60 | spring-cloud-contract-oa3
61 | 2.0.1
62 |
63 |
64 |
65 | ```
66 |
67 | ## Defining Contracts in OpenAPI
68 |
69 | Natively, OpenAPI does a great job of describing an API in a holistic manner.
70 |
71 | OpenAPI, however, does not define API interactions. Within the native OpenAPI specification, it is not possible to
72 | define request / response pairs. To define a contract, you need to define the API and the specific details of a
73 | request, and the expected response.
74 |
75 | The Open API Specification defines a number of extension points in the API. These extension points may be used to
76 | define details about request / response pairs.
77 |
78 | Complete details of OpenAPI 3.x extensions can be
79 | found [here](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#specificationExtensions).
80 |
81 | In general, most OpenAPI schema objects may be extended using objects using a property with starts with 'x-'. The extension
82 | property is an object, which provides the necessary flexibility to define interactions.
83 |
84 | The below snippet shows the definition of two contracts by extending the
85 | [Operation Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#operationObject) of the OA3 specification.
86 |
87 |
88 | ```yaml
89 | paths:
90 | /fraudcheck:
91 | put:
92 | summary: Perform Fraud Check
93 | x-contracts:
94 | - contractId: 1
95 | name: Should Mark Client as Fraud
96 | priority: 1
97 | - contractId: 2
98 | name: Should Not Mark Client as Fraud
99 | ```
100 |
101 | The OA3 extension objects are used to define request / response pairs. *While* the OA3 objects are used to define
102 | the API itself. Where ever possible, the _DRY Principle_ is followed (Don't Repeat Yourself).
103 |
104 | For example:
105 |
106 | * *Path*: Source - OA3
107 | * *HTTP Method*: Source - OA3
108 | * *Parameter Value for Interaction*: Source - OA3 Extension
109 | * *Request Body for Interaction*: Source - OA3 Extension
110 |
111 | `x-contracts` - This is the root extension object used to define contracts. This object will always expect a list of objects. Each object in
112 | the list will have a `contractId` property.
113 |
114 | The `x-contracts` object may be applied to:
115 |
116 | * [Operation Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#operationObject) - Used to define
117 | individual contacts, and header level information for contracts.
118 |
119 | * [Parameter Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#parameterObject) - Define Parameter
120 | (path, query, header, cookie) Values for interactions.
121 |
122 | * [Request Body](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#requestBodyObject) - Define the request
123 | body for interaction.
124 |
125 | * [Response Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#responseObject) - Define
126 | expected response for given interaction.
127 |
128 | ### OA3 Extensions for Spring Cloud Contract
129 | Under the covers, the converter is converting from the OA3 object format, to the `YamlContract` object of Spring Cloud Contract.
130 | This is then converted to a `Contract` object using the same converter used by Spring Cloud Contract for it's
131 | YAML DSL.
132 |
133 | The YAML DSL of Spring Cloud Contract is very robust. Please review the capabilities of the YAML DSL in the official
134 | [Spring Cloud Contract documentation](https://cloud.spring.io/spring-cloud-contract/multi/multi__contract_dsl.html#_common_top_level_elements).
135 |
136 | As much as practical, the object properties and names follow the YAML DSL of Spring Cloud Contract.
137 | #### Operation Object Extension
138 |
139 | ```json
140 | {
141 | "$schema": "http://json-schema.org/draft-04/schema#",
142 | "type": "object",
143 | "properties": {
144 | "x-contracts": {
145 | "type": "array",
146 | "items": [
147 | {
148 | "type": "object",
149 | "properties": {
150 | "contractId": {
151 | "type": "integer"
152 | },
153 | "name": {
154 | "type": "string"
155 | },
156 | "description": {
157 | "type": "string"
158 | },
159 | "label": {
160 | "type": "string"
161 | },
162 | "priority": {
163 | "type": "integer"
164 | },
165 | "ignored": {
166 | "type": "boolean"
167 | },
168 | "contractPath": {
169 | "type": "string"
170 | }
171 | },
172 | "required": [
173 | "contractId",
174 | "name",
175 | "description",
176 | "label",
177 | "priority",
178 | "ignored",
179 | "contractPath"
180 | ]
181 | },
182 | {
183 | "type": "object",
184 | "properties": {
185 | "contractId": {
186 | "type": "integer"
187 | },
188 | "name": {
189 | "type": "string"
190 | },
191 | "description": {
192 | "type": "string"
193 | },
194 | "label": {
195 | "type": "string"
196 | },
197 | "priority": {
198 | "type": "integer"
199 | },
200 | "contractPath": {
201 | "type": "string"
202 | }
203 | },
204 | "required": [
205 | "contractId",
206 | "description"
207 | ]
208 | }
209 | ]
210 | }
211 | },
212 | "required": [
213 | "x-contracts"
214 | ]
215 | }
216 | ```
217 | #### Parameter Object Extension
218 | Note: Query Parameters maybe defined on the Parameter object, or within the parameter element of the Request Body extension.
219 | ```json
220 | {
221 | "$schema": "http://json-schema.org/draft-04/schema#",
222 | "type": "object",
223 | "properties": {
224 | "x-contracts": {
225 | "type": "array",
226 | "items": [
227 | {
228 | "type": "object",
229 | "properties": {
230 | "contractId": {
231 | "type": "integer"
232 | },
233 | "value": {
234 | "type": "string"
235 | },
236 | "matchers": {
237 | "type": "array",
238 | "items": [
239 | {
240 | "type": "object",
241 | "properties": {
242 | "type": {
243 | "type": "string"
244 | },
245 | "value": {
246 | "type": "string"
247 | }
248 | },
249 | "required": []
250 | }
251 | ]
252 | }
253 | },
254 | "required": [
255 | "contractId",
256 | "value"
257 | ]
258 | }
259 | ]
260 | }
261 | },
262 | "required": [
263 | "x-contracts"
264 | ]
265 | }
266 | ```
267 | #### Request Body Extension
268 | ```json
269 | {
270 | "$schema": "http://json-schema.org/draft-04/schema#",
271 | "type": "object",
272 | "properties": {
273 | "x-contracts": {
274 | "type": "array",
275 | "items": [
276 | {
277 | "type": "object",
278 | "properties": {
279 | "contractId": {
280 | "type": "integer"
281 | },
282 | "request": {
283 | "type": "object",
284 | "properties": {
285 | "queryParameters": {
286 | "type": "array",
287 | "items": [
288 | {
289 | "type": "object",
290 | "properties": {
291 | "key": {
292 | "type": "string"
293 | },
294 | "value": {
295 | "type": "integer"
296 | }
297 | },
298 | "required": [
299 | "key",
300 | "value"
301 | ]
302 | }
303 | ]
304 | }
305 | },
306 | "required": []
307 | },
308 | "headers": {
309 | "type": "object",
310 | "properties": {
311 | "Header-key": {
312 | "type": "string"
313 | }
314 | },
315 | "required": [
316 | "Header-key"
317 | ]
318 | },
319 | "body": {
320 | "type": "object"
321 | },
322 | "multipart": {
323 | "type": "object",
324 | "named": {
325 | "type": "array",
326 | "items": [
327 | {
328 | "type": "object",
329 | "properties": {
330 | "paramName": {
331 | "type": "string"
332 | },
333 | "fileName": {
334 | "type": "string"
335 | },
336 | "fileContent": {
337 | "type": "string"
338 | }
339 | },
340 | "required": [
341 | "paramName",
342 | "fileName",
343 | "fileContent"
344 | ]
345 | }
346 | ]
347 | }
348 | },
349 | "required": [
350 | "params",
351 | "named"
352 | ]
353 | },
354 | "matchers": {
355 | "type": "object",
356 | "properties": {
357 | "headers": {
358 | "type": "array",
359 | "items": [
360 | {
361 | "type": "object",
362 | "properties": {
363 | "key": {
364 | "type": "string"
365 | },
366 | "regex": {
367 | "type": "string"
368 | },
369 | "predefined": {
370 | "type": "string"
371 | },
372 | "command": {
373 | "type": "string"
374 | },
375 | "type": {
376 | "type": "string"
377 | }
378 | },
379 | "required": []
380 | }
381 | ]
382 | },
383 | "body": {
384 | "type": "array",
385 | "items": [
386 | {
387 | "type": "object",
388 | "properties": {
389 | "path": {
390 | "type": "string"
391 | },
392 | "type": {
393 | "type": "string"
394 | },
395 | "predefined": {
396 | "type": "string"
397 | }
398 | },
399 | "required": []
400 | },
401 | {
402 | "type": "object",
403 | "properties": {
404 | "path": {
405 | "type": "string"
406 | },
407 | "type": {
408 | "type": "string"
409 | },
410 | "predefined": {
411 | "type": "string"
412 | }
413 | },
414 | "required": []
415 | },
416 | {
417 | "type": "object",
418 | "properties": {
419 | "path": {
420 | "type": "string"
421 | },
422 | "type": {
423 | "type": "string"
424 | },
425 | "predefined": {
426 | "type": "string"
427 | },
428 | "value": {
429 | "type": "string"
430 | },
431 | "minOccurrence": {
432 | "type": "integer"
433 | },
434 | "maxOccurrence": {
435 | "type": "integer"
436 | },
437 | "regexType": {
438 | "type": "string"
439 | }
440 | },
441 | "required": []
442 | }
443 | ]
444 | },
445 | "queryParameters": {
446 | "type": "array",
447 | "items": [
448 | {
449 | "type": "object",
450 | "properties": {
451 | "key": {
452 | "type": "string"
453 | },
454 | "type": {
455 | "type": "string"
456 | },
457 | "value": {
458 | "type": "string"
459 | }
460 | },
461 | "required": []
462 | }
463 | ]
464 | },
465 | "cookies": {
466 | "type": "array",
467 | "items": [
468 | {
469 | "type": "object",
470 | "properties": {
471 | "key": {
472 | "type": "string"
473 | },
474 | "regex": {
475 | "type": "string"
476 | },
477 | "predefined": {
478 | "type": "string"
479 | },
480 | "command": {
481 | "type": "string"
482 | },
483 | "type": {
484 | "type": "string"
485 | }
486 | },
487 | "required": [
488 | "key",
489 | "regex",
490 | "predefined",
491 | "command",
492 | "type"
493 | ]
494 | }
495 | ]
496 | },
497 | "multipart": {
498 | "type": "object",
499 | "properties": {
500 | "params": {
501 | "type": "array",
502 | "items": [
503 | {
504 | "type": "object",
505 | "properties": {
506 | "key": {
507 | "type": "string"
508 | },
509 | "regex": {
510 | "type": "string"
511 | },
512 | "predefined": {
513 | "type": "string"
514 | },
515 | "command": {
516 | "type": "string"
517 | },
518 | "type": {
519 | "type": "string"
520 | }
521 | },
522 | "required": []
523 | }
524 | ]
525 | },
526 | "named": {
527 | "type": "array",
528 | "items": [
529 | {
530 | "type": "object",
531 | "properties": {
532 | "paramName": {
533 | "type": "string"
534 | },
535 | "fileName": {
536 | "type": "object",
537 | "properties": {
538 | "regex": {
539 | "type": "string"
540 | },
541 | "perfefined": {
542 | "type": "string"
543 | }
544 | },
545 | "required": []
546 | },
547 | "fileContent": {
548 | "type": "object",
549 | "properties": {
550 | "regex": {
551 | "type": "string"
552 | },
553 | "perfefined": {
554 | "type": "string"
555 | }
556 | },
557 | "required": [ ]
558 | },
559 | "contentType": {
560 | "type": "object",
561 | "properties": {
562 | "regex": {
563 | "type": "string"
564 | },
565 | "perfefined": {
566 | "type": "string"
567 | }
568 | },
569 | "required": [ ]
570 | }
571 | },
572 | "required": []
573 | }
574 | ]
575 | }
576 | },
577 | "required": [
578 | "params",
579 | "named"
580 | ]
581 | }
582 | },
583 | "required": []
584 | }
585 | },
586 | "required": []
587 | }
588 | ]
589 | }
590 | },
591 | "required": [
592 | "x-contracts"
593 | ]
594 | }
595 | ```
596 |
597 | #### Response Object Extension
598 | ```json
599 | {
600 | "$schema": "http://json-schema.org/draft-04/schema#",
601 | "type": "object",
602 | "properties": {
603 | "x-contracts": {
604 | "type": "array",
605 | "items": [
606 | {
607 | "type": "object",
608 | "properties": {
609 | "contractId": {
610 | "type": "integer"
611 | },
612 | "headers": {
613 | "type": "object",
614 | "properties": {
615 | "HeaderKey": {
616 | "type": "string"
617 | }
618 | },
619 | "required": [
620 | "HeaderKey"
621 | ]
622 | },
623 | "body": {
624 | "type": "object"
625 | },
626 | "cookies": {
627 | "type": "object",
628 | "properties": {
629 | "key": {
630 | "type": "string"
631 | }
632 | },
633 | "required": [
634 | "key"
635 | ]
636 | },
637 | "assyc": {
638 | "type": "boolean"
639 | },
640 | "fixedDelayMilliseconds": {
641 | "type": "integer"
642 | },
643 | "matchers": {
644 | "type": "object",
645 | "properties": {
646 | "headers": {
647 | "type": "array",
648 | "items": [
649 | {
650 | "type": "object",
651 | "properties": {
652 | "key": {
653 | "type": "string"
654 | },
655 | "regex": {
656 | "type": "string"
657 | },
658 | "command": {
659 | "type": "string"
660 | },
661 | "predefined": {
662 | "type": "string"
663 | },
664 | "regexType": {
665 | "type": "string"
666 | }
667 | },
668 | "required": []
669 | }
670 | ]
671 | },
672 | "body": {
673 | "type": "array",
674 | "items": [
675 | {
676 | "type": "object",
677 | "properties": {
678 | "path": {
679 | "type": "string"
680 | },
681 | "type": {
682 | "type": "string"
683 | },
684 | "predefined": {
685 | "type": "string"
686 | },
687 | "value": {
688 | "type": "string"
689 | },
690 | "minOccurrence": {
691 | "type": "integer"
692 | },
693 | "maxOccurrence": {
694 | "type": "integer"
695 | },
696 | "regexType": {
697 | "type": "string"
698 | }
699 | },
700 | "required": []
701 | }
702 | ]
703 | },
704 | "cookies": {
705 | "type": "object",
706 | "properties": {
707 | "key": {
708 | "type": "string"
709 | },
710 | "regex": {
711 | "type": "string"
712 | },
713 | "command": {
714 | "type": "string"
715 | },
716 | "predefined": {
717 | "type": "string"
718 | },
719 | "regexType": {
720 | "type": "string"
721 | }
722 | },
723 | "required": []
724 | }
725 | },
726 | "required": []
727 | }
728 | },
729 | "required": [
730 | "contractId"]
731 | }
732 | ]
733 | }
734 | },
735 | "required": [
736 | "x-contracts"
737 | ]
738 | }
739 | ```
740 |
741 | ### Example Contract Definition
742 |
743 | Consider the following example:
744 |
745 | ```yaml
746 | openapi: 3.0.0
747 | info:
748 | description: Spring Cloud Contract Verifier Http Server OA3 Sample
749 | version: "1.0.0"
750 | title: Fraud Service API
751 | paths:
752 | /fraudcheck:
753 | put:
754 | summary: Perform Fraud Check
755 | x-contracts:
756 | - contractId: 1
757 | name: Should Mark Client as Fraud
758 | priority: 1
759 | - contractId: 2
760 | name: Should Not Mark Client as Fraud
761 | requestBody:
762 | content:
763 | application/json:
764 | schema:
765 | type: object
766 | properties:
767 | "client.id":
768 | type: integer
769 | loanAmount:
770 | type: integer
771 | x-contracts:
772 | - contractId: 1
773 | body:
774 | "client.id": 1234567890
775 | loanAmount: 99999
776 | matchers:
777 | body:
778 | - path: $.['client.id']
779 | type: by_regex
780 | value: "[0-9]{10}"
781 | - contractId: 2
782 | body:
783 | "client.id": 1234567890
784 | loanAmount: 123.123
785 | matchers:
786 | body:
787 | - path: $.['client.id']
788 | type: by_regex
789 | value: "[0-9]{10}"
790 | responses:
791 | '200':
792 | description: created ok
793 | content:
794 | application/json:
795 | schema:
796 | type: object
797 | properties:
798 | fraudCheckStatus:
799 | type: string
800 | "rejection.reason":
801 | type: string
802 | x-contracts:
803 | - contractId: 1
804 | body:
805 | fraudCheckStatus: "FRAUD"
806 | "rejection.reason": "Amount too high"
807 | headers:
808 | Content-Type: application/json;charset=UTF-8
809 | - contractId: 2
810 | body:
811 | fraudCheckStatus: "OK"
812 | "rejection.reason": null
813 | headers:
814 | Content-Type: application/json;charset=UTF-8
815 | matchers:
816 | body:
817 | - path: $.['rejection.reason']
818 | type: by_command
819 | value: assertThatRejectionReasonIsNull($it)
820 | /frauds:
821 | get:
822 | x-contracts:
823 | - contractId: 3
824 | name: should return all frauds - should count all frauds
825 | responses:
826 | '200':
827 | description: okay
828 | content:
829 | application/json:
830 | schema:
831 | type: object
832 | properties:
833 | count:
834 | type: integer
835 | x-contracts:
836 | - contractId: 3
837 | body:
838 | count: 200
839 | /drunks:
840 | get:
841 | x-contracts:
842 | - contractId: 6
843 | name: drunk frauds
844 | responses:
845 | '200':
846 | description: okay
847 | content:
848 | application/json:
849 | schema:
850 | type: object
851 | properties:
852 | count:
853 | type: integer
854 | x-contracts:
855 | - contractId: 6
856 | body:
857 | count: 100
858 | ```
859 |
860 | #### Define Contract Headers
861 |
862 | Two Contracts are defined in the Operation Object:
863 |
864 | ```yaml
865 | put:
866 | summary: Perform Fraud Check
867 | x-contracts:
868 | - contractId: 1
869 | name: Should Mark Client as Fraud
870 | priority: 1
871 | - contractId: 2
872 | name: Should Not Mark Client as Fraud
873 | ```
874 |
875 | #### Define Expected Request for Contacts
876 |
877 | In the Request Body Object, the details for the expected request for each contract are given:
878 |
879 | ```yaml
880 | requestBody:
881 | content:
882 | application/json:
883 | schema:
884 | type: object
885 | properties:
886 | "client.id":
887 | type: integer
888 | loanAmount:
889 | type: integer
890 | x-contracts:
891 | - contractId: 1
892 | body:
893 | "client.id": 1234567890
894 | loanAmount: 99999
895 | matchers:
896 | body:
897 | - path: $.['client.id']
898 | type: by_regex
899 | value: "[0-9]{10}"
900 | - contractId: 2
901 | body:
902 | "client.id": 1234567890
903 | loanAmount: 123.123
904 | matchers:
905 | body:
906 | - path: $.['client.id']
907 | type: by_regex
908 | value: "[0-9]{10}"
909 | ```
910 |
911 | *Note:* Notice how `x-contracts` is a list, with two objects, each of which has a `contractId` property.
912 | The `contractId` property is matched to the `contractId` property in other sections of the document.
913 |
914 | #### Define Expected Responses for Each Contract
915 |
916 | The expected response for each contract, is defined on the
917 | https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#responseObject[Response Object].
918 |
919 | In this example, two responses are defined for the HTTP status of 200.
920 |
921 | ```yaml
922 | responses:
923 | '200':
924 | description: created ok
925 | content:
926 | application/json:
927 | schema:
928 | type: object
929 | properties:
930 | fraudCheckStatus:
931 | type: string
932 | "rejection.reason":
933 | type: string
934 | x-contracts:
935 | - contractId: 1
936 | body:
937 | fraudCheckStatus: "FRAUD"
938 | "rejection.reason": "Amount too high"
939 | headers:
940 | Content-Type: application/json;charset=UTF-8
941 | - contractId: 2
942 | body:
943 | fraudCheckStatus: "OK"
944 | "rejection.reason": null
945 | headers:
946 | Content-Type: application/json;charset=UTF-8
947 | matchers:
948 | body:
949 | - path: $.['rejection.reason']
950 | type: by_command
951 | value: assertThatRejectionReasonIsNull($it)
952 | ```
953 |
954 | ### Advanced Example
955 |
956 | Following is a more advanced example showing how to incorporate query parameters, cookies, header values, and
957 | more detailed response properties.
958 |
959 | ```yaml
960 | openapi: "3.0.0"
961 | info:
962 | version: 1.0.0
963 | title: SCC
964 | paths:
965 | /foo:
966 | put:
967 | x-contracts:
968 | - contractId: 1
969 | description: Some description
970 | name: some name
971 | priority: 8
972 | ignored: true
973 | parameters:
974 | - name: a
975 | in: query
976 | schema:
977 | type: string
978 | x-contracts:
979 | - contractId: 1
980 | value: b
981 | - name: b
982 | in: query
983 | schema:
984 | type: string
985 | x-contracts:
986 | - contractId: 1
987 | value: c
988 | - name: foo
989 | in: header
990 | schema:
991 | type: string
992 | x-contracts:
993 | - contractId: 1
994 | value: bar
995 | - name: fooReq
996 | in: header
997 | schema:
998 | type: string
999 | x-contracts:
1000 | - contractId: 1
1001 | value: baz
1002 | - name: foo
1003 | in: cookie
1004 | schema:
1005 | type: string
1006 | x-contracts:
1007 | - contractId: 1
1008 | value: bar
1009 | - name: fooReq
1010 | in: cookie
1011 | schema:
1012 | type: string
1013 | x-contracts:
1014 | - contractId: 1
1015 | value: baz
1016 | requestBody:
1017 | content:
1018 | application/json:
1019 | schema:
1020 | properties:
1021 | foo:
1022 | type: string
1023 | x-contracts:
1024 | - contractId: 1
1025 | body:
1026 | foo: bar
1027 | matchers:
1028 | body:
1029 | - path: $.foo
1030 | type: by_regex
1031 | value: bar
1032 | headers:
1033 | - key: foo
1034 | regex: bar
1035 | responses:
1036 | '200':
1037 | description: the response
1038 | content:
1039 | application/json:
1040 | schema:
1041 | properties:
1042 | foo:
1043 | type: string
1044 | x-contracts:
1045 | - contractId: 1
1046 | headers:
1047 | foo2: bar
1048 | foo3: foo33
1049 | fooRes: baz
1050 | body:
1051 | foo2: bar
1052 | foo3: baz
1053 | nullValue: null
1054 | matchers:
1055 | body:
1056 | - path: $.foo2
1057 | type: by_regex
1058 | value: bar
1059 | - path: $.foo3
1060 | type: by_command
1061 | value: executeMe($it)
1062 | - path: $.nullValue
1063 | type: by_null
1064 | value: null
1065 | headers:
1066 | - key: foo2
1067 | regex: bar
1068 | - key: foo3
1069 | command: andMeToo($it)
1070 | cookies:
1071 | - key: foo2
1072 | regex: bar
1073 | - key: foo3
1074 | predefined:
1075 | ```
1076 |
1077 | ### OA3 YAML Syntax
1078 | The [YAML DSL for Spring Cloud Contract](https://cloud.spring.io/spring-cloud-contract/multi/multi__contract_dsl.html#contract-matchers) defines a number of advanced features (regx, matchers, json path, etc).
1079 | These features should work with the OA3 DSL by using the same YAML syntax.
1080 |
1081 | # License
1082 |
1083 | The Spring Cloud Contract OpenAPI 3.0 Contract Converter is released under version 2.0 of the [Apache License](http://www.apache.org/licenses/LICENSE-2.0).
1084 |
--------------------------------------------------------------------------------