├── 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 | [![Gitter chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/spring-cloud-contract-oa3/Lobby) 4 | 5 | [![CircleCI](https://circleci.com/gh/springframeworkguru/spring-cloud-contract-oa3.svg?style=svg)](https://circleci.com/gh/springframeworkguru/spring-cloud-contract-oa3) 6 | 7 | ![QualityGate](https://sonarcloud.io/api/project_badges/measure?project=guru.springframework%3Aspring-cloud-contract-oa3&metric=alert_status) 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 | --------------------------------------------------------------------------------