From: Kai Moritz Date: Fri, 3 Jun 2022 14:59:32 +0000 (+0200) Subject: Separated the Java Consumer es a standalone example X-Git-Url: http://juplo.de/gitweb/?a=commitdiff_plain;h=4ee46854ce9669486dc9d5fe7332c6d9b6f9f414;p=demos%2Fexample-siren Separated the Java Consumer es a standalone example * The `README.md` is not yet reworked. --- diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8f862e2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Pactflow + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b6eb530 --- /dev/null +++ b/README.md @@ -0,0 +1,263 @@ +# Example Pact + Siren project +Example project using [Siren](https://github.com/kevinswiber/siren) for hypermedia entities and testing with Pact. + +This project has two sub-projects, a provider springboot project which is using `spring-hateoas-siren` to provide Siren +responses and a Javascript consumer project using `ketting` to parse and navigate the Siren responses. + +## Provider Project + +The provider project is a springboot application with Siren support provided by `spring-hateoas-siren`. It has two +resources, a root resource which provides links to the other resources and an order resource for dealing with orders +in the system. + +### Root Resource + +This just provides the links to the other resources. + +`GET /`: + + ```json +{ + "class": [ + "representation" + ], + "links": [ + { + "rel": [ + "orders" + ], + "href": "http://localhost:8080/orders" + } + ] +} +``` + +### Order Resource + +This provides all the CRUD operations on Orders: fetch all orders, fetch an order by ID, update a resource or delete one. + +`GET /orders` + +```json +{ + "class": [ + "entity" + ], + "entities": [ + { + "class": [ + "entity" + ], + "rel": [ + "item" + ], + "properties": { + "id": 1234 + }, + "links": [ + { + "rel": [ + "self" + ], + "href": "http://localhost:8080/orders/1234" + } + ], + "actions": [ + { + "name": "update", + "method": "PUT", + "href": "http://localhost:8080/orders/1234" + }, + { + "name": "delete", + "method": "DELETE", + "href": "http://localhost:8080/orders/1234" + } + ] + } + ], + "links": [ + { + "rel": [ + "self" + ], + "href": "http://localhost:8080/orders" + } + ] +} +``` + +## Consumer Project + +This is a simple Javascript application that uses [Ketting](https://github.com/badgateway/ketting) which is a +hypermedia client for javascript. It has a single function in `consumer/src/consumer.js` that navigates the links from the provider to find the +orders resource, get all the orders, find the first one and execute the delete action. + +The consumer does the following: + +1. Get the root resource +2. Find the orders relation +3. Execute a GET to the URL of the orders relation +4. Extract the first order entity from the embedded entities +5. Find the delete action for that order +6. Execute the action (which executes a DELETE to the URL of the action) + +## Pact Tests + +The problem with using normal Pact tests to test this scenario is that Siren responses contain URLs to the resources and +actions. The URLs when running the consumer test will be different than those when verifying the provider. This will +result in a verification failure. + +To get round this problem, we use the `url` matcher function from the consumer Pact DSL. This function takes a list of +path fragments. The path fragments can be either plain strings or regular expressions. It then constructs +the actual URL to use in the consumer test using the mock servers base URL, and a regular expression matcher that can +match the URLs in the provider verification test. + +### Dealing with hypermedia formats like Siren actions + +Siren takes hypermedia links one step further by introducing resource actions. These encode the URL, HTTP method and +optionally any required parameters needed to make the requests for the actions supported by the resource. + +The problem could then arise that the consumer make only use a few actions provided by the provider. We would want to +ensure that these actions are present in the list for the resource, and ignore the ones we are not using. The other issue +is that our tests should not be dependent on the order of the actions. + +This is where the "array contains" matcher can help. It will allow us to match the resource actions for the ones we are +using, and ignore the others. It will also not depend on the order the actions are returned. + +This is the actions for the order resource in the provider: + +```json +{ + "actions": [ + { + "name": "update", + "method": "PUT", + "href": "http://localhost:8080/orders/6774860028109588394" + }, + { + "name": "delete", + "method": "DELETE", + "href": "http://localhost:8080/orders/6774860028109588394" + }, + { + "name": "changeStatus", + "method": "PUT", + "href": "http://localhost:8080/orders/6774860028109588394/status" + } + ] +} +``` + +For example, in the consumer test we can specify: + +```js +"actions": arrayContaining( + { + "name": "update", + "method": "PUT", + "href": url(["orders", regex("\\d+", "1234")]) + }, + { + "name": "delete", + "method": "DELETE", + "href": url(["orders", regex("\\d+", "1234")]) + } +) +``` + +This will match the actions if they contain the update and delete actions. it will ignore the other actions. + +You can see this in work if you remove one of the controller methods in the provider. For instance, if we commented out +the delete endpoint, and then run the pact verification in the provider, we get this error: + +```console +$ ./gradlew pactverify + +> Task :startServer + + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v2.3.4.RELEASE) + +2020-11-09 14:53:32.046 INFO 39485 --- [ main] i.p.e.s.SirenProviderApplication : Starting SirenProviderApplication on ronald-P95xER with PID 39485 (/home/ronald/Development/Projects/Pact/example-siren/provider/build/libs/siren-provider-0.0.1.jar started by ronald in /home/ronald/Development/Projects/Pact/example-siren/provider) +2020-11-09 14:53:32.048 INFO 39485 --- [ main] i.p.e.s.SirenProviderApplication : No active profile set, falling back to default profiles: default +2020-11-09 14:53:32.797 INFO 39485 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) +2020-11-09 14:53:32.808 INFO 39485 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] +2020-11-09 14:53:32.808 INFO 39485 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.38] +2020-11-09 14:53:32.870 INFO 39485 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext +2020-11-09 14:53:32.870 INFO 39485 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 759 ms +2020-11-09 14:53:33.071 INFO 39485 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' +2020-11-09 14:53:33.221 INFO 39485 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' +2020-11-09 14:53:33.229 INFO 39485 --- [ main] i.p.e.s.SirenProviderApplication : Started SirenProviderApplication in 1.53 seconds (JVM running for 1.903) +java -jar /home/ronald/Development/Projects/Pact/example-siren/provider/build/libs/siren-provider-0.0.1.jar is ready. + +> Task :pactVerify_Siren_Order_Provider FAILED + +Verifying a pact between Siren Consumer and Siren Order Provider + [Using File /home/ronald/Development/Projects/Pact/example-siren/consumer/pacts/Siren Order Provider-Siren Order Service.json] + get root + returns a response which + has status code 200 (OK) + has a matching body (OK) + get all orders + returns a response which + has status code 200 (OK) + has a matching body (FAILED) + delete order + returns a response which + has status code 200 (FAILED) + has a matching body (OK) + +NOTE: Skipping publishing of verification results as it has been disabled (pact.verifier.publishResults is not 'true') + + +Failures: + +1) Verifying a pact between Siren Consumer and Siren Order Provider - get all orders + + 1.1) body: $.entities.0.actions Variant at index 1 ({"href":http://localhost:9000/orders/1234,"method":DELETE,"name":delete}) was not found in the actual list + + [ + { + - "href": "http://localhost:9000/orders/1234", + + "href": "http://localhost:8080/orders/7779028774458252624", + "method": "PUT", + "name": "update" + }, + { + - "href": "http://localhost:9000/orders/1234", + - "method": "DELETE", + - "name": "delete" + + "href": "http://localhost:8080/orders/7779028774458252624/status", + + "method": "PUT", + + "name": "changeStatus" + } + ] + + + 1.2) status: expected status of 200 but was 405 + + + +FAILURE: Build failed with an exception. + +* What went wrong: +There were 2 non-pending pact failures for provider Siren Order Provider + +* Try: +Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights. + +* Get more help at https://help.gradle.org + +Deprecated Gradle features were used in this build, making it incompatible with Gradle 7.0. +Use '--warning-mode all' to show the individual deprecation warnings. +See https://docs.gradle.org/6.6.1/userguide/command_line_interface.html#sec:command_line_warnings + +BUILD FAILED in 4s +8 actionable tasks: 6 executed, 2 up-to-date +``` diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2927e83 --- /dev/null +++ b/pom.xml @@ -0,0 +1,65 @@ + + + + + org.springframework.boot + spring-boot-starter-parent + 2.7.0 + + + + 4.0.0 + io.pactflow.example.sirenconsumer.pact + siren-consumer + 1.0.0-SNAPSHOT + + + 11 + 4.2.2 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + au.com.dius.pact.consumer + junit5 + ${pact.version} + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + maven-failsafe-plugin + + + + + diff --git a/spring-consumer/LICENSE b/spring-consumer/LICENSE deleted file mode 100644 index 8f862e2..0000000 --- a/spring-consumer/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 Pactflow - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/spring-consumer/README.md b/spring-consumer/README.md deleted file mode 100644 index b6eb530..0000000 --- a/spring-consumer/README.md +++ /dev/null @@ -1,263 +0,0 @@ -# Example Pact + Siren project -Example project using [Siren](https://github.com/kevinswiber/siren) for hypermedia entities and testing with Pact. - -This project has two sub-projects, a provider springboot project which is using `spring-hateoas-siren` to provide Siren -responses and a Javascript consumer project using `ketting` to parse and navigate the Siren responses. - -## Provider Project - -The provider project is a springboot application with Siren support provided by `spring-hateoas-siren`. It has two -resources, a root resource which provides links to the other resources and an order resource for dealing with orders -in the system. - -### Root Resource - -This just provides the links to the other resources. - -`GET /`: - - ```json -{ - "class": [ - "representation" - ], - "links": [ - { - "rel": [ - "orders" - ], - "href": "http://localhost:8080/orders" - } - ] -} -``` - -### Order Resource - -This provides all the CRUD operations on Orders: fetch all orders, fetch an order by ID, update a resource or delete one. - -`GET /orders` - -```json -{ - "class": [ - "entity" - ], - "entities": [ - { - "class": [ - "entity" - ], - "rel": [ - "item" - ], - "properties": { - "id": 1234 - }, - "links": [ - { - "rel": [ - "self" - ], - "href": "http://localhost:8080/orders/1234" - } - ], - "actions": [ - { - "name": "update", - "method": "PUT", - "href": "http://localhost:8080/orders/1234" - }, - { - "name": "delete", - "method": "DELETE", - "href": "http://localhost:8080/orders/1234" - } - ] - } - ], - "links": [ - { - "rel": [ - "self" - ], - "href": "http://localhost:8080/orders" - } - ] -} -``` - -## Consumer Project - -This is a simple Javascript application that uses [Ketting](https://github.com/badgateway/ketting) which is a -hypermedia client for javascript. It has a single function in `consumer/src/consumer.js` that navigates the links from the provider to find the -orders resource, get all the orders, find the first one and execute the delete action. - -The consumer does the following: - -1. Get the root resource -2. Find the orders relation -3. Execute a GET to the URL of the orders relation -4. Extract the first order entity from the embedded entities -5. Find the delete action for that order -6. Execute the action (which executes a DELETE to the URL of the action) - -## Pact Tests - -The problem with using normal Pact tests to test this scenario is that Siren responses contain URLs to the resources and -actions. The URLs when running the consumer test will be different than those when verifying the provider. This will -result in a verification failure. - -To get round this problem, we use the `url` matcher function from the consumer Pact DSL. This function takes a list of -path fragments. The path fragments can be either plain strings or regular expressions. It then constructs -the actual URL to use in the consumer test using the mock servers base URL, and a regular expression matcher that can -match the URLs in the provider verification test. - -### Dealing with hypermedia formats like Siren actions - -Siren takes hypermedia links one step further by introducing resource actions. These encode the URL, HTTP method and -optionally any required parameters needed to make the requests for the actions supported by the resource. - -The problem could then arise that the consumer make only use a few actions provided by the provider. We would want to -ensure that these actions are present in the list for the resource, and ignore the ones we are not using. The other issue -is that our tests should not be dependent on the order of the actions. - -This is where the "array contains" matcher can help. It will allow us to match the resource actions for the ones we are -using, and ignore the others. It will also not depend on the order the actions are returned. - -This is the actions for the order resource in the provider: - -```json -{ - "actions": [ - { - "name": "update", - "method": "PUT", - "href": "http://localhost:8080/orders/6774860028109588394" - }, - { - "name": "delete", - "method": "DELETE", - "href": "http://localhost:8080/orders/6774860028109588394" - }, - { - "name": "changeStatus", - "method": "PUT", - "href": "http://localhost:8080/orders/6774860028109588394/status" - } - ] -} -``` - -For example, in the consumer test we can specify: - -```js -"actions": arrayContaining( - { - "name": "update", - "method": "PUT", - "href": url(["orders", regex("\\d+", "1234")]) - }, - { - "name": "delete", - "method": "DELETE", - "href": url(["orders", regex("\\d+", "1234")]) - } -) -``` - -This will match the actions if they contain the update and delete actions. it will ignore the other actions. - -You can see this in work if you remove one of the controller methods in the provider. For instance, if we commented out -the delete endpoint, and then run the pact verification in the provider, we get this error: - -```console -$ ./gradlew pactverify - -> Task :startServer - - . ____ _ __ _ _ - /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ -( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ - \\/ ___)| |_)| | | | | || (_| | ) ) ) ) - ' |____| .__|_| |_|_| |_\__, | / / / / - =========|_|==============|___/=/_/_/_/ - :: Spring Boot :: (v2.3.4.RELEASE) - -2020-11-09 14:53:32.046 INFO 39485 --- [ main] i.p.e.s.SirenProviderApplication : Starting SirenProviderApplication on ronald-P95xER with PID 39485 (/home/ronald/Development/Projects/Pact/example-siren/provider/build/libs/siren-provider-0.0.1.jar started by ronald in /home/ronald/Development/Projects/Pact/example-siren/provider) -2020-11-09 14:53:32.048 INFO 39485 --- [ main] i.p.e.s.SirenProviderApplication : No active profile set, falling back to default profiles: default -2020-11-09 14:53:32.797 INFO 39485 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) -2020-11-09 14:53:32.808 INFO 39485 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] -2020-11-09 14:53:32.808 INFO 39485 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.38] -2020-11-09 14:53:32.870 INFO 39485 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext -2020-11-09 14:53:32.870 INFO 39485 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 759 ms -2020-11-09 14:53:33.071 INFO 39485 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' -2020-11-09 14:53:33.221 INFO 39485 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' -2020-11-09 14:53:33.229 INFO 39485 --- [ main] i.p.e.s.SirenProviderApplication : Started SirenProviderApplication in 1.53 seconds (JVM running for 1.903) -java -jar /home/ronald/Development/Projects/Pact/example-siren/provider/build/libs/siren-provider-0.0.1.jar is ready. - -> Task :pactVerify_Siren_Order_Provider FAILED - -Verifying a pact between Siren Consumer and Siren Order Provider - [Using File /home/ronald/Development/Projects/Pact/example-siren/consumer/pacts/Siren Order Provider-Siren Order Service.json] - get root - returns a response which - has status code 200 (OK) - has a matching body (OK) - get all orders - returns a response which - has status code 200 (OK) - has a matching body (FAILED) - delete order - returns a response which - has status code 200 (FAILED) - has a matching body (OK) - -NOTE: Skipping publishing of verification results as it has been disabled (pact.verifier.publishResults is not 'true') - - -Failures: - -1) Verifying a pact between Siren Consumer and Siren Order Provider - get all orders - - 1.1) body: $.entities.0.actions Variant at index 1 ({"href":http://localhost:9000/orders/1234,"method":DELETE,"name":delete}) was not found in the actual list - - [ - { - - "href": "http://localhost:9000/orders/1234", - + "href": "http://localhost:8080/orders/7779028774458252624", - "method": "PUT", - "name": "update" - }, - { - - "href": "http://localhost:9000/orders/1234", - - "method": "DELETE", - - "name": "delete" - + "href": "http://localhost:8080/orders/7779028774458252624/status", - + "method": "PUT", - + "name": "changeStatus" - } - ] - - - 1.2) status: expected status of 200 but was 405 - - - -FAILURE: Build failed with an exception. - -* What went wrong: -There were 2 non-pending pact failures for provider Siren Order Provider - -* Try: -Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights. - -* Get more help at https://help.gradle.org - -Deprecated Gradle features were used in this build, making it incompatible with Gradle 7.0. -Use '--warning-mode all' to show the individual deprecation warnings. -See https://docs.gradle.org/6.6.1/userguide/command_line_interface.html#sec:command_line_warnings - -BUILD FAILED in 4s -8 actionable tasks: 6 executed, 2 up-to-date -``` diff --git a/spring-consumer/pom.xml b/spring-consumer/pom.xml deleted file mode 100644 index 2927e83..0000000 --- a/spring-consumer/pom.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - org.springframework.boot - spring-boot-starter-parent - 2.7.0 - - - - 4.0.0 - io.pactflow.example.sirenconsumer.pact - siren-consumer - 1.0.0-SNAPSHOT - - - 11 - 4.2.2 - - - - - org.springframework.boot - spring-boot-starter-web - - - org.projectlombok - lombok - true - - - org.springframework.boot - spring-boot-starter-test - test - - - au.com.dius.pact.consumer - junit5 - ${pact.version} - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - maven-failsafe-plugin - - - - - diff --git a/spring-consumer/src/main/java/io/pactflow/example/sirenconsumer/Application.java b/spring-consumer/src/main/java/io/pactflow/example/sirenconsumer/Application.java deleted file mode 100644 index fee9494..0000000 --- a/spring-consumer/src/main/java/io/pactflow/example/sirenconsumer/Application.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.pactflow.example.sirenconsumer; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class Application -{ - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } - -} diff --git a/spring-consumer/src/test/java/io/pactflow/example/sirenconsumer/ApplicationTests.java b/spring-consumer/src/test/java/io/pactflow/example/sirenconsumer/ApplicationTests.java deleted file mode 100644 index d1ca158..0000000 --- a/spring-consumer/src/test/java/io/pactflow/example/sirenconsumer/ApplicationTests.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.pactflow.example.sirenconsumer; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ApplicationTests -{ - - @Test - void contextLoads() { - } - -} diff --git a/spring-consumer/src/test/java/io/pactflow/example/sirenconsumer/ContractTest.java b/spring-consumer/src/test/java/io/pactflow/example/sirenconsumer/ContractTest.java deleted file mode 100644 index 16235cc..0000000 --- a/spring-consumer/src/test/java/io/pactflow/example/sirenconsumer/ContractTest.java +++ /dev/null @@ -1,143 +0,0 @@ -package io.pactflow.example.sirenconsumer; - -import au.com.dius.pact.consumer.MockServer; -import au.com.dius.pact.consumer.dsl.*; -import au.com.dius.pact.consumer.junit5.PactConsumerTestExt; -import au.com.dius.pact.consumer.junit5.PactTestFor; -import au.com.dius.pact.core.model.RequestResponsePact; -import au.com.dius.pact.core.model.annotations.Pact; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.web.client.RestTemplate; - -import java.util.Map; - -import static org.assertj.core.api.Assertions.fail; - - -@ExtendWith(PactConsumerTestExt.class) -@PactTestFor(providerName = "SirenOrderProvider") -public class ContractTest -{ - @Pact(consumer="SpringConsumer") - public RequestResponsePact deletesTheFirstOrderUsingtheDeleteAction(PactDslWithProvider builder) - { - return builder - .uponReceiving("get root") - .path("/") - .method("GET") - .willRespondWith() - .status(200) - .headers(Map.of("Content-Type", "application/vnd.siren+json")) - .body(LambdaDsl.newJsonBody(body -> - { - body.array("class", classArray -> - { - classArray.stringValue("representation"); - }); - body.array("links", linksArray -> - { - linksArray.object(object-> - { - object.matchUrl2("href", "orders"); - object.array("rel", relArray -> - { - relArray.stringValue("orders"); - }); - }); - }); - }).build()) - .uponReceiving("get all orders") - .path("/orders") - .method("GET") - .willRespondWith() - .status(200) - .headers(Map.of("Content-Type", "application/vnd.siren+json")) - .body(LambdaDsl.newJsonBody(body -> - { - body.array("class", classArray -> - { - classArray.stringValue("entity"); - }); - body.eachLike("entities", entities -> - { - entities.arrayContaining("actions", actionsArray-> - { - actionsArray.object(object -> - { - object.stringValue("name","update"); - object.stringValue("method", "PUT"); - object.matchUrl2("href", "orders", Matchers.regexp("\\d+", "1234").getMatcher()); - }); - actionsArray.object(object -> - { - object.stringValue("name","delete"); - object.stringValue("method", "DELETE"); - object.matchUrl2("href", "orders", Matchers.regexp("\\d+", "1234").getMatcher()); - }); - }); - entities.array("class", classArray -> - { - classArray.stringValue("entity"); - }); - entities.array("links", linksArray -> - { - linksArray.object(object-> - { - object.matchUrl2("href", "orders", Matchers.regexp("\\d+", "1234").getMatcher()); - object.array("rel", relArray -> - { - relArray.stringValue("self"); - }); - }); - }); - entities.object("properties", object-> - { - object.integerType("id", 1234); - }); - entities.array("rel", relArray -> - { - relArray.stringValue("item"); - }); - }); - body.array("links", linksArray -> - { - linksArray.object(object-> - { - object.matchUrl2("href", "orders"); - object.array("rel", relArray -> - { - relArray.stringValue("self"); - }); - }); - }); - }).build()) - .uponReceiving("delete order") - .matchPath("/orders/\\d+", "/orders/1234") - .method("DELETE") - .willRespondWith() - .status(200) - .toPact(); - } - - @Test - @PactTestFor(pactMethod = "deletesTheFirstOrderUsingtheDeleteAction") - public void testDeletesTheFirstOrderUsingtheDeleteAction(MockServer mockServer) - { - RestTemplate restTemplate = - new RestTemplateBuilder() - .rootUri(mockServer.getUrl()) - .build(); - try - { - restTemplate.getForEntity("/", String.class); - restTemplate.getForEntity("/orders", String.class); - restTemplate.delete("/orders/1234"); - } - catch (Exception e) - { - fail("Unexpected exception", e); - } - } -} diff --git a/spring-consumer/target/pacts/SpringConsumer-SirenOrderProvider.json b/spring-consumer/target/pacts/SpringConsumer-SirenOrderProvider.json deleted file mode 100644 index a8f8b03..0000000 --- a/spring-consumer/target/pacts/SpringConsumer-SirenOrderProvider.json +++ /dev/null @@ -1,260 +0,0 @@ -{ - "consumer": { - "name": "SpringConsumer" - }, - "interactions": [ - { - "description": "delete order", - "request": { - "matchingRules": { - "path": { - "combine": "AND", - "matchers": [ - { - "match": "regex", - "regex": "/orders/\\d+" - } - ] - } - }, - "method": "DELETE", - "path": "/orders/1234" - }, - "response": { - "status": 200 - } - }, - { - "description": "get all orders", - "request": { - "method": "GET", - "path": "/orders" - }, - "response": { - "body": { - "class": [ - "entity" - ], - "entities": [ - { - "actions": [ - { - "href": "http://localhost:8080/orders/1234", - "method": "PUT", - "name": "update" - }, - { - "href": "http://localhost:8080/orders/1234", - "method": "DELETE", - "name": "delete" - } - ], - "class": [ - "entity" - ], - "links": [ - { - "href": "http://localhost:8080/orders/1234", - "rel": [ - "self" - ] - } - ], - "properties": { - "id": 1234 - }, - "rel": [ - "item" - ] - } - ], - "links": [ - { - "href": "http://localhost:8080/orders", - "rel": [ - "self" - ] - } - ] - }, - "generators": { - "body": { - "$.entities[*].links[0].href": { - "example": "http://localhost:8080/orders/1234", - "regex": ".*\\/(\\Qorders\\E\\/\\d+)$", - "type": "MockServerURL" - }, - "$.entities[*][0].href": { - "example": "http://localhost:8080/orders/1234", - "regex": ".*\\/(\\Qorders\\E\\/\\d+)$", - "type": "MockServerURL" - }, - "$.entities[*][1].href": { - "example": "http://localhost:8080/orders/1234", - "regex": ".*\\/(\\Qorders\\E\\/\\d+)$", - "type": "MockServerURL" - }, - "$.links[0].href": { - "example": "http://localhost:8080/orders", - "regex": ".*\\/(\\Qorders\\E)$", - "type": "MockServerURL" - } - } - }, - "headers": { - "Content-Type": "application/vnd.siren+json" - }, - "matchingRules": { - "body": { - "$.entities": { - "combine": "AND", - "matchers": [ - { - "match": "type" - } - ] - }, - "$.entities[*].actions": { - "combine": "AND", - "matchers": [ - { - "match": "arrayContains", - "variants": [ - { - "generators": { - "$.href": { - "example": "http://localhost:8080/orders/1234", - "regex": ".*\\/(\\Qorders\\E\\/\\d+)$", - "type": "MockServerURL" - } - }, - "index": 0, - "rules": { - "$.href": { - "combine": "AND", - "matchers": [ - { - "match": "regex", - "regex": ".*\\/(\\Qorders\\E\\/\\d+)$" - } - ] - } - } - }, - { - "generators": { - "$.href": { - "example": "http://localhost:8080/orders/1234", - "regex": ".*\\/(\\Qorders\\E\\/\\d+)$", - "type": "MockServerURL" - } - }, - "index": 1, - "rules": { - "$.href": { - "combine": "AND", - "matchers": [ - { - "match": "regex", - "regex": ".*\\/(\\Qorders\\E\\/\\d+)$" - } - ] - } - } - } - ] - } - ] - }, - "$.entities[*].links[0].href": { - "combine": "AND", - "matchers": [ - { - "match": "regex", - "regex": ".*\\/(\\Qorders\\E\\/\\d+)$" - } - ] - }, - "$.entities[*].properties.id": { - "combine": "AND", - "matchers": [ - { - "match": "integer" - } - ] - }, - "$.links[0].href": { - "combine": "AND", - "matchers": [ - { - "match": "regex", - "regex": ".*\\/(\\Qorders\\E)$" - } - ] - } - } - }, - "status": 200 - } - }, - { - "description": "get root", - "request": { - "method": "GET", - "path": "/" - }, - "response": { - "body": { - "class": [ - "representation" - ], - "links": [ - { - "href": "http://localhost:8080/orders", - "rel": [ - "orders" - ] - } - ] - }, - "generators": { - "body": { - "$.links[0].href": { - "example": "http://localhost:8080/orders", - "regex": ".*\\/(\\Qorders\\E)$", - "type": "MockServerURL" - } - } - }, - "headers": { - "Content-Type": "application/vnd.siren+json" - }, - "matchingRules": { - "body": { - "$.links[0].href": { - "combine": "AND", - "matchers": [ - { - "match": "regex", - "regex": ".*\\/(\\Qorders\\E)$" - } - ] - } - } - }, - "status": 200 - } - } - ], - "metadata": { - "pact-jvm": { - "version": "4.2.2" - }, - "pactSpecification": { - "version": "3.0.0" - } - }, - "provider": { - "name": "SirenOrderProvider" - } -} diff --git a/src/main/java/io/pactflow/example/sirenconsumer/Application.java b/src/main/java/io/pactflow/example/sirenconsumer/Application.java new file mode 100644 index 0000000..fee9494 --- /dev/null +++ b/src/main/java/io/pactflow/example/sirenconsumer/Application.java @@ -0,0 +1,14 @@ +package io.pactflow.example.sirenconsumer; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application +{ + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/src/test/java/io/pactflow/example/sirenconsumer/ApplicationTests.java b/src/test/java/io/pactflow/example/sirenconsumer/ApplicationTests.java new file mode 100644 index 0000000..d1ca158 --- /dev/null +++ b/src/test/java/io/pactflow/example/sirenconsumer/ApplicationTests.java @@ -0,0 +1,14 @@ +package io.pactflow.example.sirenconsumer; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ApplicationTests +{ + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/io/pactflow/example/sirenconsumer/ContractTest.java b/src/test/java/io/pactflow/example/sirenconsumer/ContractTest.java new file mode 100644 index 0000000..16235cc --- /dev/null +++ b/src/test/java/io/pactflow/example/sirenconsumer/ContractTest.java @@ -0,0 +1,143 @@ +package io.pactflow.example.sirenconsumer; + +import au.com.dius.pact.consumer.MockServer; +import au.com.dius.pact.consumer.dsl.*; +import au.com.dius.pact.consumer.junit5.PactConsumerTestExt; +import au.com.dius.pact.consumer.junit5.PactTestFor; +import au.com.dius.pact.core.model.RequestResponsePact; +import au.com.dius.pact.core.model.annotations.Pact; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.fail; + + +@ExtendWith(PactConsumerTestExt.class) +@PactTestFor(providerName = "SirenOrderProvider") +public class ContractTest +{ + @Pact(consumer="SpringConsumer") + public RequestResponsePact deletesTheFirstOrderUsingtheDeleteAction(PactDslWithProvider builder) + { + return builder + .uponReceiving("get root") + .path("/") + .method("GET") + .willRespondWith() + .status(200) + .headers(Map.of("Content-Type", "application/vnd.siren+json")) + .body(LambdaDsl.newJsonBody(body -> + { + body.array("class", classArray -> + { + classArray.stringValue("representation"); + }); + body.array("links", linksArray -> + { + linksArray.object(object-> + { + object.matchUrl2("href", "orders"); + object.array("rel", relArray -> + { + relArray.stringValue("orders"); + }); + }); + }); + }).build()) + .uponReceiving("get all orders") + .path("/orders") + .method("GET") + .willRespondWith() + .status(200) + .headers(Map.of("Content-Type", "application/vnd.siren+json")) + .body(LambdaDsl.newJsonBody(body -> + { + body.array("class", classArray -> + { + classArray.stringValue("entity"); + }); + body.eachLike("entities", entities -> + { + entities.arrayContaining("actions", actionsArray-> + { + actionsArray.object(object -> + { + object.stringValue("name","update"); + object.stringValue("method", "PUT"); + object.matchUrl2("href", "orders", Matchers.regexp("\\d+", "1234").getMatcher()); + }); + actionsArray.object(object -> + { + object.stringValue("name","delete"); + object.stringValue("method", "DELETE"); + object.matchUrl2("href", "orders", Matchers.regexp("\\d+", "1234").getMatcher()); + }); + }); + entities.array("class", classArray -> + { + classArray.stringValue("entity"); + }); + entities.array("links", linksArray -> + { + linksArray.object(object-> + { + object.matchUrl2("href", "orders", Matchers.regexp("\\d+", "1234").getMatcher()); + object.array("rel", relArray -> + { + relArray.stringValue("self"); + }); + }); + }); + entities.object("properties", object-> + { + object.integerType("id", 1234); + }); + entities.array("rel", relArray -> + { + relArray.stringValue("item"); + }); + }); + body.array("links", linksArray -> + { + linksArray.object(object-> + { + object.matchUrl2("href", "orders"); + object.array("rel", relArray -> + { + relArray.stringValue("self"); + }); + }); + }); + }).build()) + .uponReceiving("delete order") + .matchPath("/orders/\\d+", "/orders/1234") + .method("DELETE") + .willRespondWith() + .status(200) + .toPact(); + } + + @Test + @PactTestFor(pactMethod = "deletesTheFirstOrderUsingtheDeleteAction") + public void testDeletesTheFirstOrderUsingtheDeleteAction(MockServer mockServer) + { + RestTemplate restTemplate = + new RestTemplateBuilder() + .rootUri(mockServer.getUrl()) + .build(); + try + { + restTemplate.getForEntity("/", String.class); + restTemplate.getForEntity("/orders", String.class); + restTemplate.delete("/orders/1234"); + } + catch (Exception e) + { + fail("Unexpected exception", e); + } + } +} diff --git a/target/pacts/SpringConsumer-SirenOrderProvider.json b/target/pacts/SpringConsumer-SirenOrderProvider.json new file mode 100644 index 0000000..a8f8b03 --- /dev/null +++ b/target/pacts/SpringConsumer-SirenOrderProvider.json @@ -0,0 +1,260 @@ +{ + "consumer": { + "name": "SpringConsumer" + }, + "interactions": [ + { + "description": "delete order", + "request": { + "matchingRules": { + "path": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": "/orders/\\d+" + } + ] + } + }, + "method": "DELETE", + "path": "/orders/1234" + }, + "response": { + "status": 200 + } + }, + { + "description": "get all orders", + "request": { + "method": "GET", + "path": "/orders" + }, + "response": { + "body": { + "class": [ + "entity" + ], + "entities": [ + { + "actions": [ + { + "href": "http://localhost:8080/orders/1234", + "method": "PUT", + "name": "update" + }, + { + "href": "http://localhost:8080/orders/1234", + "method": "DELETE", + "name": "delete" + } + ], + "class": [ + "entity" + ], + "links": [ + { + "href": "http://localhost:8080/orders/1234", + "rel": [ + "self" + ] + } + ], + "properties": { + "id": 1234 + }, + "rel": [ + "item" + ] + } + ], + "links": [ + { + "href": "http://localhost:8080/orders", + "rel": [ + "self" + ] + } + ] + }, + "generators": { + "body": { + "$.entities[*].links[0].href": { + "example": "http://localhost:8080/orders/1234", + "regex": ".*\\/(\\Qorders\\E\\/\\d+)$", + "type": "MockServerURL" + }, + "$.entities[*][0].href": { + "example": "http://localhost:8080/orders/1234", + "regex": ".*\\/(\\Qorders\\E\\/\\d+)$", + "type": "MockServerURL" + }, + "$.entities[*][1].href": { + "example": "http://localhost:8080/orders/1234", + "regex": ".*\\/(\\Qorders\\E\\/\\d+)$", + "type": "MockServerURL" + }, + "$.links[0].href": { + "example": "http://localhost:8080/orders", + "regex": ".*\\/(\\Qorders\\E)$", + "type": "MockServerURL" + } + } + }, + "headers": { + "Content-Type": "application/vnd.siren+json" + }, + "matchingRules": { + "body": { + "$.entities": { + "combine": "AND", + "matchers": [ + { + "match": "type" + } + ] + }, + "$.entities[*].actions": { + "combine": "AND", + "matchers": [ + { + "match": "arrayContains", + "variants": [ + { + "generators": { + "$.href": { + "example": "http://localhost:8080/orders/1234", + "regex": ".*\\/(\\Qorders\\E\\/\\d+)$", + "type": "MockServerURL" + } + }, + "index": 0, + "rules": { + "$.href": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": ".*\\/(\\Qorders\\E\\/\\d+)$" + } + ] + } + } + }, + { + "generators": { + "$.href": { + "example": "http://localhost:8080/orders/1234", + "regex": ".*\\/(\\Qorders\\E\\/\\d+)$", + "type": "MockServerURL" + } + }, + "index": 1, + "rules": { + "$.href": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": ".*\\/(\\Qorders\\E\\/\\d+)$" + } + ] + } + } + } + ] + } + ] + }, + "$.entities[*].links[0].href": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": ".*\\/(\\Qorders\\E\\/\\d+)$" + } + ] + }, + "$.entities[*].properties.id": { + "combine": "AND", + "matchers": [ + { + "match": "integer" + } + ] + }, + "$.links[0].href": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": ".*\\/(\\Qorders\\E)$" + } + ] + } + } + }, + "status": 200 + } + }, + { + "description": "get root", + "request": { + "method": "GET", + "path": "/" + }, + "response": { + "body": { + "class": [ + "representation" + ], + "links": [ + { + "href": "http://localhost:8080/orders", + "rel": [ + "orders" + ] + } + ] + }, + "generators": { + "body": { + "$.links[0].href": { + "example": "http://localhost:8080/orders", + "regex": ".*\\/(\\Qorders\\E)$", + "type": "MockServerURL" + } + } + }, + "headers": { + "Content-Type": "application/vnd.siren+json" + }, + "matchingRules": { + "body": { + "$.links[0].href": { + "combine": "AND", + "matchers": [ + { + "match": "regex", + "regex": ".*\\/(\\Qorders\\E)$" + } + ] + } + } + }, + "status": 200 + } + } + ], + "metadata": { + "pact-jvm": { + "version": "4.2.2" + }, + "pactSpecification": { + "version": "3.0.0" + } + }, + "provider": { + "name": "SirenOrderProvider" + } +}