An open API service indexing awesome lists of open source software.

https://github.com/pactflow/example-siren

Example project using Siren for hypermedia
https://github.com/pactflow/example-siren

example

Last synced: 10 months ago
JSON representation

Example project using Siren for hypermedia

Awesome Lists containing this project

README

          

# 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
```