Skip to content

Commit

Permalink
chore: First commit, replicating pactflow-example-consumer-js-sns
Browse files Browse the repository at this point in the history
  • Loading branch information
mikegeeves committed Jun 15, 2022
0 parents commit 4720ac6
Show file tree
Hide file tree
Showing 26 changed files with 1,237 additions and 0 deletions.
Empty file added .env
Empty file.
48 changes: 48 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Build

on:
push:
workflow_dispatch:

env:
PACT_BROKER_BASE_URL: https://test.pactflow.io
PACT_BROKER_TOKEN: ${{ secrets.PACTFLOW_TOKEN_FOR_CI_CD_WORKSHOP }}
REACT_APP_API_BASE_URL: http://localhost:8080
GIT_COMMIT: ${{ github.sha }}
GIT_REF: ${{ github.ref }}

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v4
with:
python-version: '3.8'
- name: Install
run: make deps
- name: Test
run: make test
- name: Publish pacts
run: GIT_BRANCH=${GIT_REF:11} make publish_pacts

# Runs on branches as well, so we know the status of our PRs
can-i-deploy:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v2
- run: docker pull pactfoundation/pact-cli:latest
- name: Can I deploy?
run: GIT_BRANCH=${GIT_REF:11} make can_i_deploy

# Only deploy from master
deploy:
runs-on: ubuntu-latest
needs: can-i-deploy
steps:
- uses: actions/checkout@v2
- run: docker pull pactfoundation/pact-cli:latest
- name: Deploy
run: GIT_BRANCH=${GIT_REF:11} make deploy
if: github.ref == 'refs/heads/master'
27 changes: 27 additions & 0 deletions .github/workflows/trigger_partner_docs_update.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Trigger update to partners.pactflow.io

on:
push:
branches:
- master
paths:
- '**.md'

jobs:
run:
runs-on: ubuntu-latest
steps:
- name: Trigger partner docs update
if: github.repository == 'pactflow/example-consumer-js-sns'
uses: peter-evans/repository-dispatch@v1
with:
token: ${{ secrets.GHTOKENFORTRIGGERINGPACTDOCSUPDATE }}
repository: pactflow/partners.pactflow.io
event-type: pactflow-example-consumer-js-sns-updated
- name: Trigger docs update
if: github.repository == 'pactflow/example-consumer-js-sns'
uses: peter-evans/repository-dispatch@v1
with:
token: ${{ secrets.GHTOKENFORTRIGGERINGPACTDOCSUPDATE }}
repository: pactflow/docs.pactflow.io
event-type: pactflow-example-consumer-js-sns-updated
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.venv
.aws-sam
.idea
.python-version
__pycache__
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2019-2021 Pactflow

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
152 changes: 152 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Default to the read only token - the read/write token will be present on Travis CI.
# It's set as a secure environment variable in the .travis.yml file
PACTICIPANT := "pactflow-example-consumer-python-sns"
GITHUB_WEBHOOK_UUID := "c76b601e-d66a-4eb1-88a4-6ebc50c0df8b"
PACT_CLI="docker run --rm -v ${PWD}:${PWD} -e PACT_BROKER_BASE_URL -e PACT_BROKER_TOKEN pactfoundation/pact-cli:latest"

# Only deploy from master
ifeq ($(GIT_BRANCH),master)
DEPLOY_TARGET=deploy
else
DEPLOY_TARGET=no_deploy
endif

all: test

## ====================
## CI tasks
## ====================

ci: test publish_pacts can_i_deploy $(DEPLOY_TARGET)

# Run the ci target from a developer machine with the environment variables
# set as if it was on Travis CI.
# Use this for quick feedback when playing around with your workflows.
fake_ci: .env
CI=true \
GIT_COMMIT=`git rev-parse --short HEAD`+`date +%s` \
GIT_BRANCH=`git rev-parse --abbrev-ref HEAD` \
make ci


publish_pacts: .env
@"${PACT_CLI}" publish ${PWD}/pacts --consumer-app-version ${GIT_COMMIT} --tag ${GIT_BRANCH}

## =====================
## Build/test tasks
## =====================

test: .env
python3 -m pytest

## =====================
## Deploy tasks
## =====================

create_environment:
@"${PACT_CLI}" broker create-environment --name production --production

deploy: deploy_app record_deployment

no_deploy:
@echo "Not deploying as not on master branch"

can_i_deploy: .env
@"${PACT_CLI}" broker can-i-deploy \
--pacticipant ${PACTICIPANT} \
--version ${GIT_COMMIT} \
--to-environment production \
--retry-while-unknown 0 \
--retry-interval 10

deploy_app:
@echo "Deploying to production"

record_deployment: .env
@"${PACT_CLI}" broker record-deployment --pacticipant ${PACTICIPANT} --version ${GIT_COMMIT} --environment production

## =====================
## Pactflow set up tasks
## =====================

# This should be called once before creating the webhook
# with the environment variable GITHUB_TOKEN set
create_github_token_secret:
@curl -v -X POST ${PACT_BROKER_BASE_URL}/secrets \
-H "Authorization: Bearer ${PACT_BROKER_TOKEN}" \
-H "Content-Type: application/json" \
-H "Accept: application/hal+json" \
-d "{\"name\":\"githubCommitStatusToken\",\"description\":\"Github token for updating commit statuses\",\"value\":\"${GITHUB_TOKEN}\"}"

# This webhook will update the Github commit status for this commit
# so that any PRs will get a status that shows what the status of
# the pact is.
create_or_update_github_webhook:
@"${PACT_CLI}" \
broker create-or-update-webhook \
'https://api.github.com/repos/pactflow/example-consumer-js-sns/statuses/$${pactbroker.consumerVersionNumber}' \
--header 'Content-Type: application/json' 'Accept: application/vnd.github.v3+json' 'Authorization: token $${user.githubCommitStatusToken}' \
--request POST \
--data @${PWD}/pactflow/github-commit-status-webhook.json \
--uuid ${GITHUB_WEBHOOK_UUID} \
--consumer ${PACTICIPANT} \
--contract-published \
--provider-verification-published \
--description "Github commit status webhook for ${PACTICIPANT}"

test_github_webhook:
@curl -v -X POST ${PACT_BROKER_BASE_URL}/webhooks/${GITHUB_WEBHOOK_UUID}/execute -H "Authorization: Bearer ${PACT_BROKER_TOKEN}"

## ======================
## Misc
## ======================

.env:
touch .env

.PHONY: test

## ======================
## Python additions
## ======================
PROJECT := example-consumer-python-sns
PYTHON_MAJOR_VERSION := 3.8

sgr0 := $(shell tput sgr0)
red := $(shell tput setaf 1)
green := $(shell tput setaf 2)

deps:
poetry install

integration:
sam local invoke ProductEventHandler --event ./__tests__/events/update.json

venv:
@if [ -d "./.venv" ]; then echo "$(red).venv already exists, not continuing!$(sgr0)"; exit 1; fi
@type pyenv >/dev/null 2>&1 || (echo "$(red)pyenv not found$(sgr0)"; exit 1)

@echo "\n$(green)Try to find the most recent minor version of the major version specified$(sgr0)"
$(eval PYENV_VERSION=$(shell pyenv install -l | grep "\s\s$(PYTHON_MAJOR_VERSION)\.*" | tail -1 | xargs))
@echo "$(PYTHON_MAJOR_VERSION) -> $(PYENV_VERSION)"

@echo "\n$(green)Install the Python pyenv version if not already available$(sgr0)"
pyenv install $(PYENV_VERSION) -s

@echo "\n$(green)Make a .venv dir$(sgr0)"
~/.pyenv/versions/${PYENV_VERSION}/bin/python3 -m venv ${CURDIR}/.venv

@echo "\n$(green)Make it 'available' to pyenv$(sgr0)"
ln -sf ${CURDIR}/.venv ~/.pyenv/versions/${PROJECT}

@echo "\n$(green)Use it! (populate .python-version)$(sgr0)"
pyenv local ${PROJECT}

deploy:
scripts/deploy.sh

publish:
scripts/publish.sh

logs:
sam logs -n ProductEventHandler --stack-name pactflow-example-consumer-python-sns -t
90 changes: 90 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Example Python AWS SNS Consumer

[![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)

[![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)

[![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)

[![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)

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.

All examples in the series `example-consumer-<language>-sns` provide the same functionality to be easily comparable across languages.
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.

Language specific sections which differ from the canonical example only can be found below.

### How to write tests?

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.

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:

![Code Modularity](docs/ports-and-adapters.png "Code Modularity")

This code base is setup with this modularity in mind:

* [Lambda Handler](src/_lambda/product.py)
* [Event Service](src/product/product_service.py)
* Business Logic
* [Product](src/product/product.py)
* [Repository](src/product/product_repository.py)

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).

See also:

* https://dius.com.au/2017/09/22/contract-testing-serverless-and-asynchronous-applications/
* https://dius.com.au/2018/10/01/contract-testing-serverless-and-asynchronous-applications---part-2/

## Usage
### Testing

* Run the unit tests: `make test`
* Run a (local) lambda integration test: `make integration`

### Running

* Deploy the actual app: `make deploy` (see below for more background)
* Publish a test event: `make publish`
* View the lambda logs: `make logs`

Here is some sample output publishing and viewing the logs:
```
➜ example-consumer-js-sns git:(master) ✗ npm run publish <aws:pact-dev>
> product-service@1.0.0 publish /Users/matthewfellows/development/public/example-consumer-js-sns
> ./scripts/publish.sh
finding topic
have topic: arn:aws:sns:ap-southeast-2:838728264948:pactflow-example-consumer-js-sns-ProductEvent-144XVHN8QP2D3, publishing message
{
"MessageId": "735a2daa-7eaa-53d7-b362-75b0d9227708"
}
> product-service@1.0.0 logs /Users/matthewfellows/development/public/example-consumer-js-sns
> sam logs -n ProductEventHandler --stack-name pactflow-example-consumer-js-sns -t
2020/11/03/[$LATEST]df9d6b71ef1e49789f4ebca64fc19270 2020-11-03T00:25:24.984000 START RequestId: 47e97e7d-52cf-4c83-9133-545749ed2750 Version: $LATEST
2020/11/03/[$LATEST]df9d6b71ef1e49789f4ebca64fc19270 2020-11-03T00:25:25.012000 2020-11-03T00:25:24.988Z 47e97e7d-52cf-4c83-9133-545749ed2750 INFO {
Records: [
{
EventSource: 'aws:sns',
EventVersion: '1.0',
EventSubscriptionArn: 'arn:aws:sns:ap-southeast-2:838728264948:pactflow-example-consumer-js-sns-ProductEvent-144XVHN8QP2D3:efaf0845-3847-4b5d-a4b1-68f33ef524e8',
Sns: [Object]
}
]
}
2020/11/03/[$LATEST]df9d6b71ef1e49789f4ebca64fc19270 2020-11-03T00:25:25.032000 END RequestId: 47e97e7d-52cf-4c83-9133-545749ed2750
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
```

If you edit the file `./scripts/publish.sh` to remove a valid property, or upload invalid JSON you will get something like this:

```
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)"]}
2020/11/03/[$LATEST]df9d6b71ef1e49789f4ebca64fc19270 2020-11-03T00:36:23.416000 END RequestId: 3eb496cd-c663-4ae2-a717-8f261b7ad48c
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
```
6 changes: 6 additions & 0 deletions pactflow/github-commit-status-webhook.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"state": "${pactbroker.githubVerificationStatus}",
"description": "Pact Verification Tests",
"context": "${pactbroker.providerName} ${pactbroker.providerVersionTags}",
"target_url": "${pactbroker.verificationResultUrl}"
}
Loading

0 comments on commit 4720ac6

Please sign in to comment.