Skip to content

Commit d4a49f5

Browse files
committed
feat(example): working serverless example with pact. #166
1 parent 08cd73b commit d4a49f5

File tree

14 files changed

+245
-33
lines changed

14 files changed

+245
-33
lines changed

examples/messages/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
"license": "MIT",
1616
"devDependencies": {
1717
"@types/mocha": "^2.2.41",
18-
"axios": "^0.14.0",
1918
"chai": "^3.5.0",
2019
"mocha": "^3.5.3",
2120
"nyc": "^11.6.0"

examples/serverless/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
.serverless
2+
pacts

examples/serverless/README.md

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,93 @@
11
# Serverless example
22

3+
![serverless-logo](https://user-images.githubusercontent.com/53900/38163394-57ec9176-353f-11e8-80d1-b9f6d5f1773f.png)
4+
35
Fictional application running using the [Serverless](https://github.com/serverless/serverless) framework.
46

7+
The very basic architecture is as follows:
8+
9+
`[Event Provider]` -> `[SNS]` <- `[Event Consumer]`
10+
11+
## Overview
12+
<!-- TOC -->
13+
14+
- [Overview](#overview)
15+
- [Test Services with Pact](#test-services-with-pact)
16+
- [Deployment](#deployment)
17+
- [Pact Broker integration](#pact-broker-integration)
18+
- [Running deployment](#running-deployment)
19+
- [Running](#running)
20+
- [Further reading](#further-reading)
21+
22+
<!-- /TOC -->
23+
524
**Message Producer**
625

7-
Small utility that takes a request and publishes a message to an SQS queue.
26+
Small utility that when invoked, publishes an "event" message to an SQS queue.
827

928
**Message Consumer**
1029

11-
Lambda function that reads from SQS and processes the data.
30+
Lambda function that reads from SQS and processes the data - by incrementing a simple counter.
31+
32+
## Test Services with Pact
33+
34+
To run both the consumer and provider pact tests:
35+
36+
```
37+
npm t
38+
```
39+
40+
Or individually:
41+
42+
```
43+
npm run test:consumer
44+
npm run test:provider
45+
```
46+
47+
## Deployment
1248

49+
### Pact Broker integration
1350

14-
## Deploy service
51+
Using the test broker at https://test.pact.dius.com.au (user/pass: `dXfltyFMgNOFZAxr8io9wJ37iUpY42M` / `O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1`), we integrate the `can-i-deploy` facility, that ensure it is safe to deploy the consumer or provider before a change.
1552

16-
Ensure you have valid AWS credentials in your environment, and then run;
53+
Whenever we verify a contract with Pact, the results are shared with the broker, which is able to determine compatibility between components.
54+
55+
You can see the current state of verification by running one of:
56+
57+
```
58+
npm run can-i-deploy:consumer
59+
npm run can-i-deploy:provider
60+
```
61+
62+
### Running deployment
63+
64+
Ensure you have valid AWS credentials in your environment and have installed serverless framework (`npm i -g serverless`), and then run;
1765

1866
```sh
19-
export AWS_ACCOUNT_ID=1234xxxx5678 # Required by function at runtime
20-
serverless deploy -v
67+
npm run deploy
68+
```
69+
70+
This will first check with `can-i-deploy`. If you want to skip this process, you can simply run:
71+
72+
```
73+
serverless deploy -f provider
74+
serverless deploy -f consumer
2175
```
2276

23-
## How we set this up
77+
## Running
78+
79+
**Invoking the provider**
80+
81+
```sh
82+
serverless invoke -f provider -l
83+
```
2484

85+
**Watching the consumer**
2586
```sh
26-
npm install -g serverless
27-
serverless create --template aws-nodejs --path sqs-publisher
87+
serverless logs -f consumer -t
2888
```
89+
90+
## Further reading
91+
92+
For further reading and introduction into the topic of asynchronous services contract testing, see this [article](https://dius.com.au/2017/09/22/contract-testing-serverless-and-asynchronous-applications/)
93+
and our other [example](https://github.com/pact-foundation/pact-js/tree/master/examples/messages) for a more detailed overview of these concepts.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/* tslint:disable:no-unused-expression object-literal-sort-keys max-classes-per-file no-empty */
2+
const consumeEvent = require("./index").consumeEvent;
3+
const { like, term } = require("../../../dist/dsl/matchers");
4+
const { MessageConsumer, Message, synchronousBodyHandler } = require("../../../dist/pact");
5+
const path = require("path");
6+
7+
describe("Serverless consumer tests", () => {
8+
const messagePact = new MessageConsumer({
9+
consumer: "SNSPactEventConsumer",
10+
dir: path.resolve(process.cwd(), "pacts"),
11+
provider: "SNSPactEventProvider",
12+
});
13+
14+
describe("receive a pact event", () => {
15+
it("should accept a valid event", () => {
16+
return messagePact
17+
.expectsToReceive("a request to save an event")
18+
.withContent({
19+
id: like(1),
20+
event: like("something important"),
21+
type: term({ generate: "save", matcher: "^(save|update|cancel)$" }),
22+
})
23+
.withMetadata({
24+
"content-type": "application/json",
25+
})
26+
.verify(synchronousBodyHandler(consumeEvent));
27+
});
28+
});
29+
});

examples/serverless/consumer/index.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,38 @@ const AWS = require('aws-sdk');
44

55
// Consumer handler, responsible for extracting message from SNS
66
// and dealing with lambda-related things.
7-
module.exports.handler = (event, context, callback) => {
7+
const handler = (event, context, callback) => {
88
console.log("Received event from SNS");
99

1010
event.Records.forEach(e => {
1111
console.log("Event:", JSON.parse(e.Sns.Message));
12-
consumeEvent(e)
12+
consumeEvent(JSON.parse(e.Sns.Message))
1313
});
1414

1515
callback(null, {
1616
event
1717
});
1818
};
1919

20+
let eventCount = 0;
21+
2022
// Actual consumer code, has no Lambda/AWS/Protocol specific stuff
2123
// This is the thing we test in the Consumer Pact tests
2224
const consumeEvent = (event) => {
23-
24-
// save in dynamo or something...
2525
console.log('consuming event', event)
2626

27+
if (!event || !event.id) {
28+
throw new Error("Invalid event, missing fields")
29+
}
30+
31+
// You'd normally do something useful, like process it
32+
// and save it in Dynamo
33+
console.log('Event count:', ++eventCount);
34+
35+
return eventCount;
2736
}
37+
38+
module.exports = {
39+
handler,
40+
consumeEvent
41+
};

examples/serverless/package.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "serverless-pact-example",
3+
"version": "1.0.0",
4+
"description": "Example testing a serverless app with Pact",
5+
"main": "index.js",
6+
"scripts": {
7+
"clean": "if [ -d 'pacts' ]; then rm -rf pacts; fi",
8+
"test": "npm run test:consumer && npm run test:publish && npm run test:provider",
9+
"test:consumer": "mocha consumer/*.spec.js",
10+
"test:provider": "mocha -t 10000 provider/*.spec.js",
11+
"test:publish": "node publish.js",
12+
"can-i-deploy": "npm run can-i-deploy:consumer && npm run can-i-deploy:provider",
13+
"can-i-deploy:consumer": "$(find ../../ -name pact-broker | grep -e 'bin/pact-broker$' | head -n 1) can-i-deploy --pacticipant SNSPactEventConsumer --latest --broker-base-url https://test.pact.dius.com.au --broker-username dXfltyFMgNOFZAxr8io9wJ37iUpY42M --broker-password O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1",
14+
"can-i-deploy:provider": "$(find ../../ -name pact-broker | grep -e 'bin/pact-broker$' | head -n 1) can-i-deploy --pacticipant SNSPactEventProvider --latest --broker-base-url https://test.pact.dius.com.au --broker-username dXfltyFMgNOFZAxr8io9wJ37iUpY42M --broker-password O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1",
15+
"deploy": "npm run deploy:consumer && npm run deploy:provider",
16+
"deploy:consumer": "npm run can-i-deploy && serverless deploy -f consumer",
17+
"deploy:provider": "npm run can-i-deploy && serverless deploy -f provider"
18+
},
19+
"devDependencies": {
20+
"chai": "^3.5.0",
21+
"mocha": "^3.5.3"
22+
},
23+
"keywords": [
24+
"pact",
25+
"serverless",
26+
"lambda",
27+
"contract-testing"
28+
],
29+
"author": "Matt Fellows <matt.fellows@onegeek.com.au>",
30+
"license": "MIT",
31+
"dependencies": {
32+
"aws-sdk": "^2.218.1"
33+
}
34+
}

examples/serverless/publisher/index.js renamed to examples/serverless/provider/index.js

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ const TOPIC_ARN = process.env.TOPIC_ARN;
77
// Handler is the Lambda and SNS specific code
88
// The message generation logic is separated from the handler itself
99
// in the
10-
module.exports.handler = (event, context, callback) => {
11-
const message = createEvent(event);
10+
const handler = (event, context, callback) => {
11+
const message = createEvent();
1212

1313
const sns = new AWS.SNS();
1414

1515
const params = {
16-
Message: message.body,
16+
Message: JSON.stringify(message),
1717
TopicArn: TOPIC_ARN
1818
};
1919

@@ -22,7 +22,6 @@ module.exports.handler = (event, context, callback) => {
2222
callback(error);
2323
}
2424

25-
console.log("Message successfully published to queue")
2625
callback(null, {
2726
message: 'Message successfully published to SNS topic "pact-events"',
2827
event
@@ -34,12 +33,16 @@ module.exports.handler = (event, context, callback) => {
3433

3534
// Separate your producer code, from the lambda handler.
3635
// No Lambda/AWS/Protocol specific stuff in here..
37-
const createEvent = (event) => {
36+
const createEvent = (obj) => {
37+
// Change 'type' to something else to test a pact failure
3838
return {
39-
statusCode: 200,
40-
body: JSON.stringify({
41-
message: 'Go Serverless v1.0! Your function executed successfully!',
42-
input: event,
43-
}),
39+
id: parseInt(Math.random() * 100),
40+
event: "an update to something useful",
41+
type: "update"
4442
};
4543
};
44+
45+
module.exports = {
46+
handler,
47+
createEvent,
48+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
2+
/* tslint:disable:no-unused-expression object-literal-sort-keys max-classes-per-file no-empty */
3+
const { like, term } = require("../../../dist/dsl/matchers");
4+
const { MessageProvider, Message } = require("../../../dist/pact");
5+
const path = require("path");
6+
const { createEvent } = require("./index");
7+
8+
describe("Message provider tests", () => {
9+
const p = new MessageProvider({
10+
handlers: {
11+
"a request to save an event": () => createEvent(),
12+
},
13+
logLevel: "WARN",
14+
provider: "SNSPactEventProvider",
15+
providerVersion: "1.0.0",
16+
17+
// For local validation
18+
// pactUrls: [path.resolve(process.cwd(), "pacts", "snspacteventconsumer-snspacteventprovider.json")],
19+
20+
// Uncomment to use the broker
21+
pactBrokerUrl: "https://test.pact.dius.com.au/",
22+
pactBrokerUsername: "dXfltyFMgNOFZAxr8io9wJ37iUpY42M",
23+
pactBrokerPassword: "O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1",
24+
publishVerificationResult: true,
25+
26+
// Tag the contract
27+
tags: ["latest"],
28+
});
29+
30+
describe("send an event", () => {
31+
it("should send a valid event", () => {
32+
return p.verify();
33+
});
34+
});
35+
});

examples/serverless/publish.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const pact = require('@pact-foundation/pact-node')
2+
const path = require('path')
3+
const opts = {
4+
pactFilesOrDirs: [path.resolve(__dirname, 'pacts/')],
5+
pactBroker: 'https://test.pact.dius.com.au',
6+
pactBrokerUsername: 'dXfltyFMgNOFZAxr8io9wJ37iUpY42M',
7+
pactBrokerPassword: 'O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1',
8+
tags: ['latest'],
9+
consumerVersion: '1.0.1'
10+
}
11+
12+
pact.publishPacts(opts)
13+
.then(() => {
14+
console.log('Pact contract publishing complete!')
15+
console.log('')
16+
console.log('Head over to https://test.pact.dius.com.au/ and login with')
17+
console.log('=> Username: dXfltyFMgNOFZAxr8io9wJ37iUpY42M')
18+
console.log('=> Password: O5AIZWxelWbLvqMd8PkAVycBJh2Psyg1')
19+
console.log('to see your published contracts.')
20+
})
21+
.catch(e => {
22+
console.log('Pact contract publishing failed: ', e)
23+
})

0 commit comments

Comments
 (0)