Skip to content

Commit 4720ac6

Browse files
committed
chore: First commit, replicating pactflow-example-consumer-js-sns
0 parents  commit 4720ac6

26 files changed

+1237
-0
lines changed

.env

Whitespace-only changes.

.github/workflows/build.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Build
2+
3+
on:
4+
push:
5+
workflow_dispatch:
6+
7+
env:
8+
PACT_BROKER_BASE_URL: https://test.pactflow.io
9+
PACT_BROKER_TOKEN: ${{ secrets.PACTFLOW_TOKEN_FOR_CI_CD_WORKSHOP }}
10+
REACT_APP_API_BASE_URL: http://localhost:8080
11+
GIT_COMMIT: ${{ github.sha }}
12+
GIT_REF: ${{ github.ref }}
13+
14+
jobs:
15+
test:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v2
19+
- uses: actions/setup-python@v4
20+
with:
21+
python-version: '3.8'
22+
- name: Install
23+
run: make deps
24+
- name: Test
25+
run: make test
26+
- name: Publish pacts
27+
run: GIT_BRANCH=${GIT_REF:11} make publish_pacts
28+
29+
# Runs on branches as well, so we know the status of our PRs
30+
can-i-deploy:
31+
runs-on: ubuntu-latest
32+
needs: test
33+
steps:
34+
- uses: actions/checkout@v2
35+
- run: docker pull pactfoundation/pact-cli:latest
36+
- name: Can I deploy?
37+
run: GIT_BRANCH=${GIT_REF:11} make can_i_deploy
38+
39+
# Only deploy from master
40+
deploy:
41+
runs-on: ubuntu-latest
42+
needs: can-i-deploy
43+
steps:
44+
- uses: actions/checkout@v2
45+
- run: docker pull pactfoundation/pact-cli:latest
46+
- name: Deploy
47+
run: GIT_BRANCH=${GIT_REF:11} make deploy
48+
if: github.ref == 'refs/heads/master'
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Trigger update to partners.pactflow.io
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
paths:
8+
- '**.md'
9+
10+
jobs:
11+
run:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Trigger partner docs update
15+
if: github.repository == 'pactflow/example-consumer-js-sns'
16+
uses: peter-evans/repository-dispatch@v1
17+
with:
18+
token: ${{ secrets.GHTOKENFORTRIGGERINGPACTDOCSUPDATE }}
19+
repository: pactflow/partners.pactflow.io
20+
event-type: pactflow-example-consumer-js-sns-updated
21+
- name: Trigger docs update
22+
if: github.repository == 'pactflow/example-consumer-js-sns'
23+
uses: peter-evans/repository-dispatch@v1
24+
with:
25+
token: ${{ secrets.GHTOKENFORTRIGGERINGPACTDOCSUPDATE }}
26+
repository: pactflow/docs.pactflow.io
27+
event-type: pactflow-example-consumer-js-sns-updated

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.venv
2+
.aws-sam
3+
.idea
4+
.python-version
5+
__pycache__

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2019-2021 Pactflow
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Makefile

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Default to the read only token - the read/write token will be present on Travis CI.
2+
# It's set as a secure environment variable in the .travis.yml file
3+
PACTICIPANT := "pactflow-example-consumer-python-sns"
4+
GITHUB_WEBHOOK_UUID := "c76b601e-d66a-4eb1-88a4-6ebc50c0df8b"
5+
PACT_CLI="docker run --rm -v ${PWD}:${PWD} -e PACT_BROKER_BASE_URL -e PACT_BROKER_TOKEN pactfoundation/pact-cli:latest"
6+
7+
# Only deploy from master
8+
ifeq ($(GIT_BRANCH),master)
9+
DEPLOY_TARGET=deploy
10+
else
11+
DEPLOY_TARGET=no_deploy
12+
endif
13+
14+
all: test
15+
16+
## ====================
17+
## CI tasks
18+
## ====================
19+
20+
ci: test publish_pacts can_i_deploy $(DEPLOY_TARGET)
21+
22+
# Run the ci target from a developer machine with the environment variables
23+
# set as if it was on Travis CI.
24+
# Use this for quick feedback when playing around with your workflows.
25+
fake_ci: .env
26+
CI=true \
27+
GIT_COMMIT=`git rev-parse --short HEAD`+`date +%s` \
28+
GIT_BRANCH=`git rev-parse --abbrev-ref HEAD` \
29+
make ci
30+
31+
32+
publish_pacts: .env
33+
@"${PACT_CLI}" publish ${PWD}/pacts --consumer-app-version ${GIT_COMMIT} --tag ${GIT_BRANCH}
34+
35+
## =====================
36+
## Build/test tasks
37+
## =====================
38+
39+
test: .env
40+
python3 -m pytest
41+
42+
## =====================
43+
## Deploy tasks
44+
## =====================
45+
46+
create_environment:
47+
@"${PACT_CLI}" broker create-environment --name production --production
48+
49+
deploy: deploy_app record_deployment
50+
51+
no_deploy:
52+
@echo "Not deploying as not on master branch"
53+
54+
can_i_deploy: .env
55+
@"${PACT_CLI}" broker can-i-deploy \
56+
--pacticipant ${PACTICIPANT} \
57+
--version ${GIT_COMMIT} \
58+
--to-environment production \
59+
--retry-while-unknown 0 \
60+
--retry-interval 10
61+
62+
deploy_app:
63+
@echo "Deploying to production"
64+
65+
record_deployment: .env
66+
@"${PACT_CLI}" broker record-deployment --pacticipant ${PACTICIPANT} --version ${GIT_COMMIT} --environment production
67+
68+
## =====================
69+
## Pactflow set up tasks
70+
## =====================
71+
72+
# This should be called once before creating the webhook
73+
# with the environment variable GITHUB_TOKEN set
74+
create_github_token_secret:
75+
@curl -v -X POST ${PACT_BROKER_BASE_URL}/secrets \
76+
-H "Authorization: Bearer ${PACT_BROKER_TOKEN}" \
77+
-H "Content-Type: application/json" \
78+
-H "Accept: application/hal+json" \
79+
-d "{\"name\":\"githubCommitStatusToken\",\"description\":\"Github token for updating commit statuses\",\"value\":\"${GITHUB_TOKEN}\"}"
80+
81+
# This webhook will update the Github commit status for this commit
82+
# so that any PRs will get a status that shows what the status of
83+
# the pact is.
84+
create_or_update_github_webhook:
85+
@"${PACT_CLI}" \
86+
broker create-or-update-webhook \
87+
'https://api.github.com/repos/pactflow/example-consumer-js-sns/statuses/$${pactbroker.consumerVersionNumber}' \
88+
--header 'Content-Type: application/json' 'Accept: application/vnd.github.v3+json' 'Authorization: token $${user.githubCommitStatusToken}' \
89+
--request POST \
90+
--data @${PWD}/pactflow/github-commit-status-webhook.json \
91+
--uuid ${GITHUB_WEBHOOK_UUID} \
92+
--consumer ${PACTICIPANT} \
93+
--contract-published \
94+
--provider-verification-published \
95+
--description "Github commit status webhook for ${PACTICIPANT}"
96+
97+
test_github_webhook:
98+
@curl -v -X POST ${PACT_BROKER_BASE_URL}/webhooks/${GITHUB_WEBHOOK_UUID}/execute -H "Authorization: Bearer ${PACT_BROKER_TOKEN}"
99+
100+
## ======================
101+
## Misc
102+
## ======================
103+
104+
.env:
105+
touch .env
106+
107+
.PHONY: test
108+
109+
## ======================
110+
## Python additions
111+
## ======================
112+
PROJECT := example-consumer-python-sns
113+
PYTHON_MAJOR_VERSION := 3.8
114+
115+
sgr0 := $(shell tput sgr0)
116+
red := $(shell tput setaf 1)
117+
green := $(shell tput setaf 2)
118+
119+
deps:
120+
poetry install
121+
122+
integration:
123+
sam local invoke ProductEventHandler --event ./__tests__/events/update.json
124+
125+
venv:
126+
@if [ -d "./.venv" ]; then echo "$(red).venv already exists, not continuing!$(sgr0)"; exit 1; fi
127+
@type pyenv >/dev/null 2>&1 || (echo "$(red)pyenv not found$(sgr0)"; exit 1)
128+
129+
@echo "\n$(green)Try to find the most recent minor version of the major version specified$(sgr0)"
130+
$(eval PYENV_VERSION=$(shell pyenv install -l | grep "\s\s$(PYTHON_MAJOR_VERSION)\.*" | tail -1 | xargs))
131+
@echo "$(PYTHON_MAJOR_VERSION) -> $(PYENV_VERSION)"
132+
133+
@echo "\n$(green)Install the Python pyenv version if not already available$(sgr0)"
134+
pyenv install $(PYENV_VERSION) -s
135+
136+
@echo "\n$(green)Make a .venv dir$(sgr0)"
137+
~/.pyenv/versions/${PYENV_VERSION}/bin/python3 -m venv ${CURDIR}/.venv
138+
139+
@echo "\n$(green)Make it 'available' to pyenv$(sgr0)"
140+
ln -sf ${CURDIR}/.venv ~/.pyenv/versions/${PROJECT}
141+
142+
@echo "\n$(green)Use it! (populate .python-version)$(sgr0)"
143+
pyenv local ${PROJECT}
144+
145+
deploy:
146+
scripts/deploy.sh
147+
148+
publish:
149+
scripts/publish.sh
150+
151+
logs:
152+
sam logs -n ProductEventHandler --stack-name pactflow-example-consumer-python-sns -t

README.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Example Python AWS SNS Consumer
2+
3+
[![Build Status](https://github.com/pactflow/example-consumer-python-sns/actions/workflows/build.yml/badge.svg)](https://github.com/pactflow/example-consumer-python-sns/actions)
4+
5+
[![Can I deploy Status](https://test.pactflow.io/pacticipants/pactflow-example-consumer-python-sns/branches/master/latest-version/can-i-deploy/to-environment/production/badge.svg)](https://test.pactflow.io/pacticipants/pactflow-example-consumer-python-sns/branches/master/latest-version/can-i-deploy/to-environment/production/badge)
6+
7+
[![Pact Status](https://test.pactflow.io/pacts/provider/pactflow-example-provider-python-sns/consumer/pactflow-example-consumer-python-sns/latest/badge.svg?label=consumer)](https://test.pactflow.io/pacts/provider/pactflow-example-provider-python-sns/consumer/pactflow-example-consumer-python-sns/latest) (latest pact)
8+
9+
[![Pact Status](https://test.pactflow.io/matrix/provider/pactflow-example-provider-python-sns/latest/master/consumer/pactflow-example-consumer-python-sns/latest/master/badge.svg?label=consumer)](https://test.pactflow.io/pacts/provider/pactflow-example-provider-python-sns/consumer/pactflow-example-consumer-python-sns/latest/prod) (prod/prod pact)
10+
11+
This is an example of a Python AWS SNS consumer that uses Pact, [Pactflow](https://pactflow.io) and GitHub Actions to ensure that it is compatible with the expectations its consumers have of it.
12+
13+
All examples in the series `example-consumer-<language>-sns` provide the same functionality to be easily comparable across languages.
14+
As such, please refer to [https://docs.pactflow.io/docs/examples/aws/sns/consumer/](AWS SNS Consumer Examples) to avoid unnecessary duplication of details here.
15+
16+
Language specific sections which differ from the canonical example only can be found below.
17+
18+
### How to write tests?
19+
20+
We recommend that you split the code that is responsible for handling the protocol specific things - in this case the lambda and SNS input - and the piece of code that actually handles the payload.
21+
22+
You're probably familiar with layered architectures such as Ports and Adaptors (also referred to as a Hexagonal architecture). Following a modular architecture will allow you to do this much more easily:
23+
24+
![Code Modularity](docs/ports-and-adapters.png "Code Modularity")
25+
26+
This code base is setup with this modularity in mind:
27+
28+
* [Lambda Handler](src/_lambda/product.py)
29+
* [Event Service](src/product/product_service.py)
30+
* Business Logic
31+
* [Product](src/product/product.py)
32+
* [Repository](src/product/product_repository.py)
33+
34+
The target of our [consumer pact test](tests/unit/product_service_pact_test.py) is the [Event Service](src/product/product_service.js), which is responsible for consuming a Product update event, and persisting it to a database (the Repository).
35+
36+
See also:
37+
38+
* https://dius.com.au/2017/09/22/contract-testing-serverless-and-asynchronous-applications/
39+
* https://dius.com.au/2018/10/01/contract-testing-serverless-and-asynchronous-applications---part-2/
40+
41+
## Usage
42+
### Testing
43+
44+
* Run the unit tests: `make test`
45+
* Run a (local) lambda integration test: `make integration`
46+
47+
### Running
48+
49+
* Deploy the actual app: `make deploy` (see below for more background)
50+
* Publish a test event: `make publish`
51+
* View the lambda logs: `make logs`
52+
53+
Here is some sample output publishing and viewing the logs:
54+
```
55+
➜ example-consumer-js-sns git:(master) ✗ npm run publish <aws:pact-dev>
56+
57+
> [email protected] publish /Users/matthewfellows/development/public/example-consumer-js-sns
58+
> ./scripts/publish.sh
59+
60+
finding topic
61+
have topic: arn:aws:sns:ap-southeast-2:838728264948:pactflow-example-consumer-js-sns-ProductEvent-144XVHN8QP2D3, publishing message
62+
{
63+
"MessageId": "735a2daa-7eaa-53d7-b362-75b0d9227708"
64+
}
65+
66+
> [email protected] logs /Users/matthewfellows/development/public/example-consumer-js-sns
67+
> sam logs -n ProductEventHandler --stack-name pactflow-example-consumer-js-sns -t
68+
69+
2020/11/03/[$LATEST]df9d6b71ef1e49789f4ebca64fc19270 2020-11-03T00:25:24.984000 START RequestId: 47e97e7d-52cf-4c83-9133-545749ed2750 Version: $LATEST
70+
2020/11/03/[$LATEST]df9d6b71ef1e49789f4ebca64fc19270 2020-11-03T00:25:25.012000 2020-11-03T00:25:24.988Z 47e97e7d-52cf-4c83-9133-545749ed2750 INFO {
71+
Records: [
72+
{
73+
EventSource: 'aws:sns',
74+
EventVersion: '1.0',
75+
EventSubscriptionArn: 'arn:aws:sns:ap-southeast-2:838728264948:pactflow-example-consumer-js-sns-ProductEvent-144XVHN8QP2D3:efaf0845-3847-4b5d-a4b1-68f33ef524e8',
76+
Sns: [Object]
77+
}
78+
]
79+
}
80+
2020/11/03/[$LATEST]df9d6b71ef1e49789f4ebca64fc19270 2020-11-03T00:25:25.032000 END RequestId: 47e97e7d-52cf-4c83-9133-545749ed2750
81+
2020/11/03/[$LATEST]df9d6b71ef1e49789f4ebca64fc19270 2020-11-03T00:25:25.032000 REPORT RequestId: 47e97e7d-52cf-4c83-9133-545749ed2750 Duration: 48.28 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 64 MB Init Duration: 136.98 ms
82+
```
83+
84+
If you edit the file `./scripts/publish.sh` to remove a valid property, or upload invalid JSON you will get something like this:
85+
86+
```
87+
2020/11/03/[$LATEST]df9d6b71ef1e49789f4ebca64fc19270 2020-11-03T00:36:23.376000 2020-11-03T00:36:23.376Z 3eb496cd-c663-4ae2-a717-8f261b7ad48c ERROR Invoke Error {"errorType":"AssertionError","errorMessage":"id is a mandatory field","code":"ERR_ASSERTION","generatedMessage":false,"expected":true,"operator":"==","stack":["AssertionError [ERR_ASSERTION]: id is a mandatory field"," at new Product (/var/task/src/product/product.js:5:5)"," at handler (/var/task/src/product/product.handler.js:7:23)"," at /var/task/src/service/product.js:10:44"," at Array.map (<anonymous>)"," at Runtime.lambda [as handler] (/var/task/src/service/product.js:10:33)"," at Runtime.handleOnce (/var/runtime/Runtime.js:66:25)"]}
88+
2020/11/03/[$LATEST]df9d6b71ef1e49789f4ebca64fc19270 2020-11-03T00:36:23.416000 END RequestId: 3eb496cd-c663-4ae2-a717-8f261b7ad48c
89+
2020/11/03/[$LATEST]df9d6b71ef1e49789f4ebca64fc19270 2020-11-03T00:36:23.416000 REPORT RequestId: 3eb496cd-c663-4ae2-a717-8f261b7ad48c Duration: 75.82 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 65 MB
90+
```
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"state": "${pactbroker.githubVerificationStatus}",
3+
"description": "Pact Verification Tests",
4+
"context": "${pactbroker.providerName} ${pactbroker.providerVersionTags}",
5+
"target_url": "${pactbroker.verificationResultUrl}"
6+
}

0 commit comments

Comments
 (0)