Skip to content

Commit 5561980

Browse files
committed
feat(proxy): add docs and e2e examples of filters and stateHandlers
1 parent 3e6e9b8 commit 5561980

File tree

7 files changed

+165
-73
lines changed

7 files changed

+165
-73
lines changed

README.md

+75-27
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Read [Getting started with Pact] for more information for beginners.
4343
- [Provider API Testing](#provider-api-testing)
4444
- [Verification Options](#verification-options)
4545
- [API with Provider States](#api-with-provider-states)
46-
- [API with Authorization](#api-with-authorization)
46+
- [Modify Requests Prior to Verification (Request Filters)](#modify-requests-prior-to-verification-request-filters)
4747
- [Publishing Pacts to a Broker](#publishing-pacts-to-a-broker)
4848
- [Publishing options](#publishing-options)
4949
- [Publishing Verification Results to a Pact Broker](#publishing-verification-results-to-a-pact-broker)
@@ -157,9 +157,6 @@ The first step is to create a test for your API Consumer. The example below uses
157157
Check out the `examples` folder for examples with Karma Jasmine, Mocha and Jest. The example below is taken from the [integration spec](https://github.com/pact-foundation/pact-js/blob/master/src/pact.integration.spec.ts).
158158

159159
```javascript
160-
/**
161-
* The following example is for Pact version 5
162-
*/
163160
const path = require("path")
164161
const chai = require("chai")
165162
const { Pact } = require("@pact-foundation/pact")
@@ -275,47 +272,98 @@ new Verifier().verifyProvider(opts).then(function () {
275272

276273
#### Verification Options
277274

278-
| Parameter | Required | Type | Description |
279-
| --------------------------- | :------: | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
280-
| `providerBaseUrl` | true | string | Running API provider host endpoint. Required. |
281-
| `provider` | true | string | Name of the Provider. Required. |
282-
| `pactUrls` | true | array of strings | Array of local Pact file paths or HTTP-based URLs (e.g. from a broker). Required if not using a Broker. |
283-
| `pactBrokerUrl` | false | string | URL of the Pact Broker to retrieve pacts from. Required if not using pactUrls. |
284-
| `tags` | false | array of strings | Array of tags, used to filter pacts from the Broker. Optional. |
285-
| `providerStatesSetupUrl` | false | string | Optional URL to call with a POST request for each `providerState` defined in a pact (see below for more info). |
286-
| `pactBrokerUsername` | false | string | Username for Pact Broker basic authentication |
287-
| `pactBrokerPassword` | false | string | Password for Pact Broker basic authentication |
288-
| `publishVerificationResult` | false | boolean | Publish verification result to Broker | boolean |
289-
| `providerVersion` | false | string | Provider version, required to publish verification results to a broker |
290-
| `customProviderHeaders` | false | array of strings | Header(s) to add to provider state set up and pact verification re`quests`. eg 'Authorization: Basic cGFjdDpwYWN0'.Broker. Optional otherwise. |
291-
| `timeout` | false | number | The duration in ms we should wait to confirm verification process was successful. Defaults to 30000, Optional. |
275+
| Parameter | Required | Type | Description |
276+
| --------------------------- | :------: | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
277+
| `providerBaseUrl` | true | string | Running API provider host endpoint. Required. |
278+
| `provider` | true | string | Name of the Provider. Required. |
279+
| `pactUrls` | true | array of strings | Array of local Pact file paths or HTTP-based URLs (e.g. from a broker). Required if not using a Broker. |
280+
| `pactBrokerUrl` | false | string | URL of the Pact Broker to retrieve pacts from. Required if not using pactUrls. |
281+
| `tags` | false | array of strings | Array of tags, used to filter pacts from the Broker. |
282+
| `providerStatesSetupUrl` | false | string | DEPRECATED (see `stateHandlers`). URL to call with a POST request for each `providerState` defined in a pact (see below for more info). |
283+
| `pactBrokerUsername` | false | string | Username for Pact Broker basic authentication |
284+
| `pactBrokerPassword` | false | string | Password for Pact Broker basic authentication |
285+
| `publishVerificationResult` | false | boolean | Publish verification result to Broker | boolean |
286+
| `providerVersion` | false | string | Provider version, required to publish verification results to a broker |
287+
| `customProviderHeaders` | false | array of strings | Header(s) to add to any requests to the provider service. eg `Authorization: Basic cGFjdDpwYWN0`. All interactions will receive the header. See `requestFilter` for when more flexiblility is required in modifying the request to the provider. |
288+
| `timeout` | false | number | The duration in ms we should wait to confirm verification process was successful. Defaults to 30000. |
289+
| `requestFilter` | false | object | An Express middleware handler (See https://expressjs.com/en/guide/writing-middleware.html) to modify requests and responses from the provider. See below for more details. |
290+
| `stateHandlers` | false | object | Provider state handlers. A map of `string` -> `() => Promise`, where each string is the state to setup, and the function is used to configure the state in the Provider. See below for detail. |
292291

293292
That's it! Read more about [Verifying Pacts](https://docs.pact.io/getting_started/verifying_pacts).
294293

295294
#### API with Provider States
296295

297-
If you have defined any `state`s in your consumer tests, the `Verifier` can put the provider into the right state right before submitting a request (for example, the provider can use the state to mock away certain database queries). To support this, the provider must provide an extra API endpoint that accepts the query parameters `consumer` and `state` and returns an HTTP `200`.
296+
If you have defined any `state`s in your consumer tests, the `Verifier` can put the provider into the right state prior to sending the request. For example, the provider can use the state to mock away certain database queries. To support this, set up a handler for each `state` using hooks on the `stateHandlers` property. Here is an example from our [e2e suite](https://github.com/pact-foundation/pact-js/blob/master/examples/e2e/test/provider.spec.js):
298297

299-
You can then configure this endpoint as `providerStatesSetupUrl` in the options passed into `Verifier.verifyProvider()`. If this option is not configured, the `Verifier` will ignore the provider states defined in the pact.
298+
```js
299+
let opts = {
300+
...
301+
stateHandlers: {
302+
"Has no animals": () => {
303+
animalRepository.clear()
304+
return Promise.resolve(`Animals removed from the db`)
305+
},
306+
"Has some animals": () => {
307+
importData()
308+
return Promise.resolve(`Animals added to the db`)
309+
},
310+
"Has an animal with ID 1": () => {
311+
importData()
312+
return Promise.resolve(`Animals added to the db`)
313+
}
314+
}
315+
}
300316

301-
See this [Provider](https://github.com/pact-foundation/pact-js/blob/master/examples/e2e/test/provider.spec.js) for a working example, or read more about [Provider States](https://docs.pact.io/getting_started/provider_states).
317+
return new Verifier(opts).verifyProvider().then(...)
318+
```
302319

303-
#### API with Authorization
320+
As you can see, for each state ("Has no animals", ...), we configure the local datastore differently. If this option is not configured, the `Verifier` will ignore the provider states defined in the pact and log a warning.
304321

305-
Sometimes you may need to add things to the requests that can't be persisted in a pact file. Examples of these would be authentication tokens, which have a small life span. e.g. an OAuth bearer token: `Authorization: Bearer 0b79bab50daca910b000d4f1a2b675d604257e42`.
322+
Read more about [Provider States](https://docs.pact.io/getting_started/provider_states).
306323

307-
For this case, we have a facility that should be carefully used during verification - the ability to specificy custom headers to be sent during provider verification. The flag to achieve this is `customProviderHeaders`.
324+
#### Modify Requests Prior to Verification (Request Filters)
308325

309-
For example, to have two headers sent as part of the verification request, modify the `verifyProvider` options as per below:
326+
Sometimes you may need to add things to the requests that can't be persisted in a pact file. Examples of these are authentication tokens with a small life span. e.g. an OAuth bearer token: `Authorization: Bearer 0b79bab50daca910b000d4f1a2b675d604257e42`.
327+
328+
For these cases, we have two facilities that should be carefully used during verification:
329+
330+
1. the ability to specify custom headers to be sent during provider verification. The flag to achieve this is `customProviderHeaders`.
331+
1. the ability to modify a request/response and modify the payload. The flag to achieve this is `requestFilter`.
332+
333+
**Example API with Authorization**
334+
335+
For example, to have an `Authorization` bearer token header sent as part of the verification request, set the `verifyProvider` options as per below:
310336

311337
```js
338+
let token
312339
let opts = {
313340
provider: 'Animal Profile Service',
314341
...
315-
customProviderHeaders: ['Authorization: Bearer e5e5e5e5e5e5e5', 'SomeSpecialHeader: some specialvalue']
342+
stateHandlers: {
343+
"is authenticated": () => {
344+
token = "1234"
345+
Promise.resolve(`Valid bearer token generated`)
346+
},
347+
"is not authenticated": () => {
348+
token = ""
349+
Promise.resolve(`Expired bearer token generated`)
350+
}
351+
},
352+
353+
// this middleware is executed for each request, allowing `token` to change between invocations
354+
// it is common to pair this with `stateHandlers` as per above, that can set/expire the token
355+
// for different test cases
356+
requestFilter: (req, res, next) => {
357+
req.headers["Authorization"] = `Bearer: ${token}`
358+
next()
359+
},
360+
361+
// This header will always be sent for each and every request, and can't be dynamic
362+
// (i.e. passing a variable instead of the bearer token)
363+
customProviderHeaders: ["Authorization: Bearer 1234"]
316364
}
317365

318-
return new Verifier().verifyProvider(opts).then(output => { ... })
366+
return new Verifier(opts).verifyProvider().then(...)
319367
```
320368

321369
As you can see, this is your opportunity to modify\add to headers being sent to the Provider API, for example to create a valid time-bound token.

examples/e2e/consumer.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,24 @@ const request = require("superagent")
33
const server = express()
44

55
const getApiEndpoint = () => process.env.API_HOST || "http://localhost:8081"
6+
const authHeader = {
7+
Authorization: "Bearer token",
8+
}
69

710
// Fetch animals who are currently 'available' from the
811
// Animal Service
912
const availableAnimals = () => {
1013
return request
1114
.get(`${getApiEndpoint()}/animals/available`)
12-
.then(res => res.body, () => [])
15+
.set(authHeader)
16+
.then(res => res.body)
1317
}
1418

1519
// Find animals by their ID from the Animal Service
1620
const getAnimalById = id => {
1721
return request
1822
.get(`${getApiEndpoint()}/animals/${id}`)
23+
.set(authHeader)
1924
.then(res => res.body, () => null)
2025
}
2126

examples/e2e/provider.js

+11
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ server.use((req, res, next) => {
1616
next()
1717
})
1818

19+
server.use((req, res, next) => {
20+
const token = req.headers["authorization"] || ""
21+
22+
if (token == "1234") {
23+
console.log("ERRRR")
24+
res.sendStatus(401).send()
25+
} else {
26+
next()
27+
}
28+
})
29+
1930
const animalRepository = new Repository()
2031

2132
// Load default data into a repository

examples/e2e/test/consumer.spec.js

+43-15
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ describe("Pact", () => {
9999

100100
// Configure and import consumer API
101101
// Note that we update the API endpoint to point at the Mock Service
102-
103102
const {
104103
createMateForDates,
105104
suggestion,
@@ -112,36 +111,63 @@ describe("Pact", () => {
112111
// use unit-style tests that test the collaborating function behaviour -
113112
// we want to test the function that is calling the external service.
114113
describe("when a call to list all animals from the Animal Service is made", () => {
115-
describe("and there are animals in the database", () => {
114+
describe("and the user is not authenticated", () => {
116115
before(() =>
117116
provider.addInteraction({
118-
state: "Has some animals",
117+
state: "is not authenticated",
119118
uponReceiving: "a request for all animals",
120119
withRequest: {
121120
method: "GET",
122121
path: "/animals/available",
123122
},
124123
willRespondWith: {
125-
status: 200,
124+
status: 401,
126125
headers: {
127126
"Content-Type": "application/json; charset=utf-8",
128127
},
129-
body: animalListExpectation,
130128
},
131129
})
132130
)
133131

134-
it("returns a list of animals", done => {
135-
const suggestedMates = suggestion(suitor)
136-
137-
expect(suggestedMates).to.eventually.have.deep.property(
138-
"suggestions[0].score",
139-
94
132+
it("returns a 401 unauthorized", () => {
133+
return expect(suggestion(suitor)).to.eventually.be.rejectedWith(
134+
"Unauthorized"
140135
)
141-
expect(suggestedMates)
142-
.to.eventually.have.property("suggestions")
143-
.with.lengthOf(MIN_ANIMALS)
144-
.notify(done)
136+
})
137+
})
138+
describe("and the user is authenticated", () => {
139+
describe("and there are animals in the database", () => {
140+
before(() =>
141+
provider.addInteraction({
142+
state: "Has some animals",
143+
uponReceiving: "a request for all animals",
144+
withRequest: {
145+
method: "GET",
146+
path: "/animals/available",
147+
headers: { Authorization: "Bearer token" },
148+
},
149+
willRespondWith: {
150+
status: 200,
151+
headers: {
152+
"Content-Type": "application/json; charset=utf-8",
153+
},
154+
body: animalListExpectation,
155+
},
156+
})
157+
)
158+
159+
it("returns a list of animals", done => {
160+
const suggestedMates = suggestion(suitor)
161+
162+
expect(suggestedMates).to.eventually.have.deep.property(
163+
"suggestions[0].score",
164+
94
165+
)
166+
expect(suggestedMates)
167+
.to.eventually.have.property("suggestions")
168+
.with.lengthOf(MIN_ANIMALS)
169+
.notify(done)
170+
})
145171
})
146172
})
147173
})
@@ -155,6 +181,7 @@ describe("Pact", () => {
155181
withRequest: {
156182
method: "GET",
157183
path: term({ generate: "/animals/1", matcher: "/animals/[0-9]+" }),
184+
headers: { Authorization: "Bearer token" },
158185
},
159186
willRespondWith: {
160187
status: 200,
@@ -183,6 +210,7 @@ describe("Pact", () => {
183210
withRequest: {
184211
method: "GET",
185212
path: "/animals/100",
213+
headers: { Authorization: "Bearer token" },
186214
},
187215
willRespondWith: {
188216
status: 404,

0 commit comments

Comments
 (0)