Git ignores the build directory of the Spring-Consumer
[demos/example-siren] / README.md
1 # Example Pact + Siren project
2 Example project using [Siren](https://github.com/kevinswiber/siren) for hypermedia entities and testing with Pact.
3
4 This project has two sub-projects, a provider springboot project which is using `spring-hateoas-siren` to provide Siren
5 responses and a Javascript consumer project using `ketting` to parse and navigate the Siren responses. 
6
7 ## Provider Project
8
9 The provider project is a springboot application with Siren support provided by `spring-hateoas-siren`. It has two
10 resources, a root resource which provides links to the other resources and an order resource for dealing with orders
11 in the system.
12
13 ### Root Resource
14
15 This just provides the links to the other resources.
16
17 `GET /`:
18
19  ```json
20 {
21   "class": [
22     "representation"
23   ],
24   "links": [
25     {
26       "rel": [
27         "orders"
28       ],
29       "href": "http://localhost:8080/orders"
30     }
31   ]
32 }
33 ```
34
35 ### Order Resource
36
37 This provides all the CRUD operations on Orders: fetch all orders, fetch an order by ID, update a resource or delete one.
38
39 `GET /orders`
40
41 ```json
42 {
43   "class": [
44     "entity"
45   ],
46   "entities": [
47     {
48       "class": [
49         "entity"
50       ],
51       "rel": [
52         "item"
53       ],
54       "properties": {
55         "id": 1234
56       },
57       "links": [
58         {
59           "rel": [
60             "self"
61           ],
62           "href": "http://localhost:8080/orders/1234"
63         }
64       ],
65       "actions": [
66         {
67           "name": "update",
68           "method": "PUT",
69           "href": "http://localhost:8080/orders/1234"
70         },
71         {
72           "name": "delete",
73           "method": "DELETE",
74           "href": "http://localhost:8080/orders/1234"
75         }
76       ]
77     }
78   ],
79   "links": [
80     {
81       "rel": [
82         "self"
83       ],
84       "href": "http://localhost:8080/orders"
85     }
86   ]
87 }
88 ```
89
90 ## Consumer Project
91
92 This is a simple Javascript application that uses [Ketting](https://github.com/badgateway/ketting) which is a 
93 hypermedia client for javascript. It has a single function in `consumer/src/consumer.js` that navigates the links from the provider to find the
94 orders resource, get all the orders, find the first one and execute the delete action.
95
96 The consumer does the following:
97
98 1. Get the root resource
99 2. Find the orders relation
100 3. Execute a GET to the URL of the orders relation
101 4. Extract the first order entity from the embedded entities
102 5. Find the delete action for that order
103 6. Execute the action (which executes a DELETE to the URL of the action) 
104
105 ## Pact Tests
106
107 The problem with using normal Pact tests to test this scenario is that Siren responses contain URLs to the resources and
108 actions. The URLs when running the consumer test will be different than those when verifying the provider. This will 
109 result in a verification failure.
110
111 To get round this problem, we use the `url` matcher function from the consumer Pact DSL. This function takes a list of 
112 path fragments. The path fragments can be either plain strings or regular expressions. It then constructs
113 the actual URL to use in the consumer test using the mock servers base URL, and a regular expression matcher that can 
114 match the URLs in the provider verification test.
115
116 ### Dealing with hypermedia formats like Siren actions
117
118 Siren takes hypermedia links one step further by introducing resource actions. These encode the URL, HTTP method and
119 optionally any required parameters needed to make the requests for the actions supported by the resource.
120
121 The problem could then arise that the consumer make only use a few actions provided by the provider. We would want to
122 ensure that these actions are present in the list for the resource, and ignore the ones we are not using. The other issue
123 is that our tests should not be dependent on the order of the actions.
124
125 This is where the "array contains" matcher can help. It will allow us to match the resource actions for the ones we are
126 using, and ignore the others. It will also not depend on the order the actions are returned.
127
128 This is the actions for the order resource in the provider:
129
130 ```json
131 {
132   "actions": [
133         {
134           "name": "update",
135           "method": "PUT",
136           "href": "http://localhost:8080/orders/6774860028109588394"
137         },
138         {
139           "name": "delete",
140           "method": "DELETE",
141           "href": "http://localhost:8080/orders/6774860028109588394"
142         },
143         {
144           "name": "changeStatus",
145           "method": "PUT",
146           "href": "http://localhost:8080/orders/6774860028109588394/status"
147         }
148   ]
149 }
150 ```
151
152 For example, in the consumer test we can specify:
153
154 ```js
155 "actions": arrayContaining(
156   {
157     "name": "update",
158     "method": "PUT",
159     "href": url(["orders", regex("\\d+", "1234")])
160   },
161   {
162     "name": "delete",
163     "method": "DELETE",
164     "href": url(["orders", regex("\\d+", "1234")])
165   }
166 )
167 ```
168
169 This will match the actions if they contain the update and delete actions. it will ignore the other actions.
170
171 You can see this in work if you remove one of the controller methods in the provider. For instance, if we commented out
172 the delete endpoint, and then run the pact verification in the provider, we get this error:
173
174 ```console
175 $ ./gradlew pactverify
176
177 > Task :startServer
178
179   .   ____          _            __ _ _
180  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
181 ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
182  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
183   '  |____| .__|_| |_|_| |_\__, | / / / /
184  =========|_|==============|___/=/_/_/_/
185  :: Spring Boot ::        (v2.3.4.RELEASE)
186
187 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)
188 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
189 2020-11-09 14:53:32.797  INFO 39485 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
190 2020-11-09 14:53:32.808  INFO 39485 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
191 2020-11-09 14:53:32.808  INFO 39485 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.38]
192 2020-11-09 14:53:32.870  INFO 39485 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
193 2020-11-09 14:53:32.870  INFO 39485 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 759 ms
194 2020-11-09 14:53:33.071  INFO 39485 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
195 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 ''
196 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)
197 java -jar /home/ronald/Development/Projects/Pact/example-siren/provider/build/libs/siren-provider-0.0.1.jar is ready.
198
199 > Task :pactVerify_Siren_Order_Provider FAILED
200
201 Verifying a pact between Siren Consumer and Siren Order Provider
202   [Using File /home/ronald/Development/Projects/Pact/example-siren/consumer/pacts/Siren Order Provider-Siren Order Service.json]
203   get root
204     returns a response which
205       has status code 200 (OK)
206       has a matching body (OK)
207   get all orders
208     returns a response which
209       has status code 200 (OK)
210       has a matching body (FAILED)
211   delete order
212     returns a response which
213       has status code 200 (FAILED)
214       has a matching body (OK)
215
216 NOTE: Skipping publishing of verification results as it has been disabled (pact.verifier.publishResults is not 'true')
217
218
219 Failures:
220
221 1) Verifying a pact between Siren Consumer and Siren Order Provider - get all orders
222
223     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
224
225         [
226           {
227         -    "href": "http://localhost:9000/orders/1234",
228         +    "href": "http://localhost:8080/orders/7779028774458252624",
229             "method": "PUT",
230             "name": "update"
231           },
232           {
233         -    "href": "http://localhost:9000/orders/1234",
234         -    "method": "DELETE",
235         -    "name": "delete"
236         +    "href": "http://localhost:8080/orders/7779028774458252624/status",
237         +    "method": "PUT",
238         +    "name": "changeStatus"
239           }
240         ]
241
242
243     1.2) status: expected status of 200 but was 405
244
245
246
247 FAILURE: Build failed with an exception.
248
249 * What went wrong:
250 There were 2 non-pending pact failures for provider Siren Order Provider
251
252 * Try:
253 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.
254
255 * Get more help at https://help.gradle.org
256
257 Deprecated Gradle features were used in this build, making it incompatible with Gradle 7.0.
258 Use '--warning-mode all' to show the individual deprecation warnings.
259 See https://docs.gradle.org/6.6.1/userguide/command_line_interface.html#sec:command_line_warnings
260
261 BUILD FAILED in 4s
262 8 actionable tasks: 6 executed, 2 up-to-date
263 ```
264
265 # Notizen
266
267 * Die Anordnung der Kontrakte ist je nach Implementierung unterschiedlich.
268 * __Dies ist kein Fehler!__
269   * Der JavaScript-Consumer erweckt den Anschein, dass die Reihenfolge
270     der Aufrufe im Kontrakt erhalten bleibt, so dass beim Testen des
271     Providers gegen den Kontrakt automatisch der benötigte Zustand
272     existiert.
273   * Dies ist aber _nicht_ so und soll so auch nicht sein.
274   * Der Provider ist in diesem Beispiel einfach so implementiert, dass er
275      gar keinen Zustand hält und nur das erwartete JSON für ausgedacht Werte
276      erzeugt.
277   * Ein Test mehrerer aufeinander folgenden Aufrufe, die sich selbst den
278      benötigten Zustand erzeugen (z.B. Erzeugen / Auflisten / Löschen) ist
279      nicht möglich und aus der Sicht von Pact auch nicht sinnvoll!
280   * Der Kotrakt soll nur sicherstellen, dass sich Consumer und Provider
281     verstehen.
282 * Für den Vergleich, den Pact normalisieren mit:
283   ```
284   cat PATH_TO_PACT | jq '.interactions|=sort_by(.description)' > NORMALIZED
285   ```