├── tests ├── __init__.py ├── conftest.py └── test_client.py ├── pact_python_demo ├── __init__.py ├── client.py └── user-app.py ├── .gitignore ├── Pipfile ├── verify_pact.sh ├── broker ├── ssl │ ├── nginx.conf │ ├── nginx-selfsigned.crt │ └── nginx-selfsigned.key └── docker-compose.yml ├── LICENSE ├── README.md └── Pipfile.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pact_python_demo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.json 2 | log/* 3 | reports/* 4 | tmp/* 5 | *.pyc 6 | *.log 7 | .cache/* 8 | .pytest_cache/* 9 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | 2 | def pytest_addoption(parser): 3 | parser.addoption( 4 | "--publish-pact", type=str, action="store", 5 | help="Upload generated pact file to pact broker with version" 6 | ) 7 | -------------------------------------------------------------------------------- /pact_python_demo/client.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | class UserClient(object): 5 | def __init__(self, base_uri): 6 | self.base_uri = base_uri 7 | 8 | def get_user(self, user_name): 9 | """Fetch a user object by user_name from the server.""" 10 | uri = self.base_uri + '/users/' + user_name 11 | response = requests.get(uri) 12 | if response.status_code == 404: 13 | return None 14 | return response.json() 15 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | atomicwrites = "*" 10 | attrs = "*" 11 | certifi = "*" 12 | chardet = "*" 13 | click = "*" 14 | idna = "*" 15 | more-itertools = "*" 16 | pact-python = "*" 17 | pipenv = "*" 18 | pluggy = "*" 19 | psutil = "*" 20 | py = "*" 21 | pytest = "*" 22 | requests = "*" 23 | six = "*" 24 | "urllib3" = "*" 25 | virtualenv = "*" 26 | virtualenv-clone = "*" 27 | flask = "*" 28 | 29 | [requires] 30 | python_version = "2.7" 31 | -------------------------------------------------------------------------------- /verify_pact.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | VERSION=$1 3 | if [ -x $VERSION ]; then 4 | echo "ERROR: You must specify a provider version" 5 | exit 6 | fi 7 | 8 | pipenv run pact-verifier --provider-base-url=http://localhost:5001 \ 9 | --pact-url="http://127.0.0.1/pacts/provider/UserService/consumer/UserServiceClient/latest" \ 10 | --provider-states-setup-url=http://localhost:5001/_pact/provider_states \ 11 | --provider-app-version $VERSION \ 12 | --pact-broker-username pactbroker \ 13 | --pact-broker-password pactbroker \ 14 | --publish-verification-results 15 | -------------------------------------------------------------------------------- /broker/ssl/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl default_server; 3 | server_name localhost; 4 | ssl_certificate /etc/nginx/ssl/nginx-selfsigned.crt; 5 | ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key; 6 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 7 | ssl_prefer_server_ciphers on; 8 | ssl_ecdh_curve secp384r1; 9 | ssl_session_cache shared:SSL:10m; 10 | ssl_stapling on; 11 | ssl_stapling_verify on; 12 | 13 | location / { 14 | proxy_pass http://broker:80; 15 | proxy_set_header Host $host; 16 | proxy_set_header X-Forwarded-Scheme "https"; 17 | proxy_set_header X-Forwarded-Port "443"; 18 | proxy_set_header X-Forwarded-Ssl "on"; 19 | proxy_set_header X-Real-IP $remote_addr; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /broker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | 5 | postgres: 6 | image: postgres 7 | healthcheck: 8 | test: psql postgres --command "select 1" -U postgres 9 | ports: 10 | - "5432:5432" 11 | environment: 12 | POSTGRES_USER: postgres 13 | POSTGRES_PASSWORD: password 14 | POSTGRES_DB: postgres 15 | 16 | broker_app: 17 | image: dius/pact-broker 18 | ports: 19 | - "80:80" 20 | links: 21 | - postgres 22 | environment: 23 | PACT_BROKER_DATABASE_USERNAME: postgres 24 | PACT_BROKER_DATABASE_PASSWORD: password 25 | PACT_BROKER_DATABASE_HOST: postgres 26 | PACT_BROKER_DATABASE_NAME: postgres 27 | PACT_BROKER_BASIC_AUTH_USERNAME: pactbroker 28 | PACT_BROKER_BASIC_AUTH_PASSWORD: pactbroker 29 | 30 | nginx: 31 | image: nginx:alpine 32 | links: 33 | - broker_app:broker 34 | volumes: 35 | - ./ssl/nginx.conf:/etc/nginx/conf.d/default.conf:ro 36 | - ./ssl:/etc/nginx/ssl 37 | ports: 38 | - "8443:443" 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Red Hat Insights 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. 22 | -------------------------------------------------------------------------------- /pact_python_demo/user-app.py: -------------------------------------------------------------------------------- 1 | #!/usr/env/bin python 2 | import datetime 3 | import json 4 | import uuid 5 | 6 | from flask import Flask, abort, jsonify, request 7 | 8 | fakedb = {} 9 | 10 | app = Flask(__name__) 11 | 12 | @app.route('/_pact/provider_states', methods=['POST']) 13 | def provider_states(): 14 | mapping = {'UserA does not exist': setup_no_user_a, 15 | 'UserA exists and is not an administrator': setup_user_a_nonadmin} 16 | mapping[request.json['state']]() 17 | return jsonify({'result': request.json['state']}) 18 | 19 | 20 | def setup_no_user_a(): 21 | if 'UserA' in fakedb: 22 | del fakedb['UserA'] 23 | 24 | 25 | def setup_user_a_nonadmin(): 26 | fakedb['UserA'] = {'name': "UserA", 'id': '1234567', 'created_on': datetime.datetime.now(), 'admin': False} 27 | 28 | 29 | @app.route('/users/') 30 | def get_user_by_name(name): 31 | user_data = fakedb.get(name) 32 | if not user_data: 33 | abort(404) 34 | response = jsonify(**user_data) 35 | app.logger.debug('get user for %s returns data:\n%s', name, response.data) 36 | return response 37 | 38 | 39 | if __name__ == '__main__': 40 | app.run(debug=True, port=5001) 41 | -------------------------------------------------------------------------------- /broker/ssl/nginx-selfsigned.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDtTCCAp2gAwIBAgIJAPVSxq8aUpd1MA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQwHhcNMTgwMTE4MjMzMjMzWhcNMTkwMTE4MjMzMjMzWjBF 5 | MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 6 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 7 | CgKCAQEA5vwTFJLlWmMC456Kv7uHxSqARkj5dwsb0lFoc5bGjOYvlJUu2S8F8QMc 8 | PlW2vXM7UsmBzbNgQsQh1FgRXgfNfHF80F21dILpzM/aA8a/GkJsKUZCHccQb5mq 9 | i25OrI4+XE6kSt/fwTbWRJGZgEADoIJ3wewcfzkJoeGonU+g8rYwPLq9xaHwFVFV 10 | x30KIZv5CToanHVy+aQUKg9m4VGuij22U7HzphS4IGZoaj2Uo0vpXqhWoKYrLFv2 11 | h0spP93Pr4VzoRGwlHgl2s+7qfwzRf9DhxePq9lGnqJ8TMQv1+jRITWB8jT8qW3Q 12 | t/+fWaGtkqbtgS8vGaAoXtXUZIWarQIDAQABo4GnMIGkMB0GA1UdDgQWBBSQCcqt 13 | DxAYpPkMWvMmU+DMT21RuDB1BgNVHSMEbjBsgBSQCcqtDxAYpPkMWvMmU+DMT21R 14 | uKFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV 15 | BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAPVSxq8aUpd1MAwGA1UdEwQF 16 | MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAE+ryTcny4NfIhdAwFbGjfZSsOHf0Ivt 17 | 5lW0hHA7SZVcPysgAlM8sk+aZDQdNEP6eoUfpuwLvAio/kpvr9pFI0THvBm9W9MS 18 | zWmKkGrsuk8MXM0SsPi+BSz49YwAtsLFku7fsr/GNAD6+Vw+fyF+ySvUYJ2FwOEF 19 | 4lYfzy1X0BA6l4RVmnO1Rv8Mn6LkzrBMe4kW0VARaBsS3hO/FW2nHMSDSkRCFjKi 20 | 4qeX6RcJhHzfsZnQi2gqbVQXUvYiSjZL561UZdaybNJdSU+uOKgK2NTU1Pv6Prkh 21 | HS9Gm+yEtgSvTx/JoLkiKx31Q4GH2hoommCT0viCcnKz1eHKGYUhMP0= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /broker/ssl/nginx-selfsigned.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEA5vwTFJLlWmMC456Kv7uHxSqARkj5dwsb0lFoc5bGjOYvlJUu 3 | 2S8F8QMcPlW2vXM7UsmBzbNgQsQh1FgRXgfNfHF80F21dILpzM/aA8a/GkJsKUZC 4 | HccQb5mqi25OrI4+XE6kSt/fwTbWRJGZgEADoIJ3wewcfzkJoeGonU+g8rYwPLq9 5 | xaHwFVFVx30KIZv5CToanHVy+aQUKg9m4VGuij22U7HzphS4IGZoaj2Uo0vpXqhW 6 | oKYrLFv2h0spP93Pr4VzoRGwlHgl2s+7qfwzRf9DhxePq9lGnqJ8TMQv1+jRITWB 7 | 8jT8qW3Qt/+fWaGtkqbtgS8vGaAoXtXUZIWarQIDAQABAoIBAQC3r5woz0yO3ZAN 8 | nSWvpZ0pwUuzGRMxhOcCEPUkfrG0mNUbrqtL0WZDLHsIYzdoXzu88TxFbbFORxSz 9 | /bkJ8uCJZuKf/PVxCy6MTnqMaD/OzSWgiRvI/GXoqeYC7ZypApE67NsgI/qXd1lb 10 | vAG7CK0ZtscvsulSjvRHBOIG/6z5dUAKnLJjr7uKydMHSIKNafKAEA6HGDCvIu4d 11 | J9EQzLfmpjLTkeB1DNZrv1mtNjf/kG/M/UX5a1RtOJTGvHQn/oZSUKng3DVUNBtq 12 | dEO6Pi5n88xWuxH6YAWqqDjCfqyey1Jc1rQxfnx6vRPL7+IaXRugAKFMFm8Xbp9/ 13 | /9eEDCyNAoGBAPZEjYH9u2856KYUTyky8gD1TOE9gf4x4zFjK6SzBT8v1y1RdSwQ 14 | tf7ozj94OV/b9bAE3k/z2a09xYty5VBXs6MCluQTS67KgRaO9sSFtRmnupyBNk2z 15 | r3QEYuVDmJ6Dk/3ovItXqFaW8IbOZMf6Acu5aEDx4UKmb2tzGGJ7DxF/AoGBAPAc 16 | 57p1yRWIG+hJMdkudXhBz+L3t2NbESWom33hi1mDMIKp3dwJmhA4kq+Uyqfl32uF 17 | Iy3z+3xr2V1BdGg1RnicfcyjHaQ4/89YB+nkOHB8muV2R57tYahOgWn6rXXxTOBs 18 | X2Vjd7ByAEFimrVfDH33inrYuIiI/cku4Xyj71HTAoGBAJeyrsBuPfFL6KW1SPYF 19 | 7dDtSchNjS+6J0sa3Z18sTS1EYVW8iiMuq8lVTb/pcgIxJUCyrbRbTssG+3EfsE4 20 | 5Oz7AVvJDwvCrjXpJtTz0BTXnzoc1giTMPb0ZL75HqA2SQlVPh9PheCg5dUEekw9 21 | ErIdqbynwqy9vVCg+1pel2+dAoGAR1C+fsIHFG8VottCg/fpies6HHZosIjWwfGf 22 | JTc9FTwCx3w+WeE8Mf8rihzOSCndPukPNtHVavH5YFpVgbH5GU+ZiZMU9ba8O9Aw 23 | oYZYQQixVN/Zi9mDfOK8S0baCELAC5QEjW+KmAx0CPeJbb8qTaudJLmDrYHKpttW 24 | u5dROGMCgYEAlgTZNiEeBAPQZD30CSvFUlZVCOOyu5crP9hCPA9um5FsvD9minSz 25 | yJqeMj7zapZsatAzYwHrGG6nHnTKWEBNaimR7kjTpKdKzXQaA9XeVLmeFAZ3Exad 26 | JDKTPI+asF+097sHUcVuloMOZXbD1uAZnvLWIwfsaHxs41AkF+0lmM4= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | """pact test for user service client""" 2 | 3 | import json 4 | import logging 5 | import os 6 | import sys 7 | 8 | import pytest 9 | import requests 10 | from requests.auth import HTTPBasicAuth 11 | 12 | from pact_python_demo.client import UserClient 13 | from pact import Consumer, Like, Provider, Term 14 | 15 | log = logging.getLogger(__name__) 16 | logging.basicConfig(level=logging.INFO) 17 | 18 | 19 | PACT_UPLOAD_URL = ( 20 | "http://127.0.0.1/pacts/provider/UserService/consumer" 21 | "/UserServiceClient/version" 22 | ) 23 | PACT_FILE = "userserviceclient-userservice.json" 24 | PACT_BROKER_USERNAME = "pactbroker" 25 | PACT_BROKER_PASSWORD = "pactbroker" 26 | 27 | PACT_MOCK_HOST = 'localhost' 28 | PACT_MOCK_PORT = 1234 29 | PACT_DIR = os.path.dirname(os.path.realpath(__file__)) 30 | 31 | @pytest.fixture 32 | def client(): 33 | return UserClient( 34 | 'http://{host}:{port}' 35 | .format(host=PACT_MOCK_HOST, port=PACT_MOCK_PORT) 36 | ) 37 | 38 | 39 | def push_to_broker(version): 40 | """TODO: see if we can dynamically learn the pact file name, version, etc.""" 41 | with open(os.path.join(PACT_DIR, PACT_FILE), 'rb') as pact_file: 42 | pact_file_json = json.load(pact_file) 43 | 44 | basic_auth = HTTPBasicAuth(PACT_BROKER_USERNAME, PACT_BROKER_PASSWORD) 45 | 46 | log.info("Uploading pact file to pact broker...") 47 | 48 | r = requests.put( 49 | "{}/{}".format(PACT_UPLOAD_URL, version), 50 | auth=basic_auth, 51 | json=pact_file_json 52 | ) 53 | if not r.ok: 54 | log.error("Error uploading: %s", r.content) 55 | r.raise_for_status() 56 | 57 | 58 | @pytest.fixture(scope='session') 59 | def pact(request): 60 | pact = Consumer('UserServiceClient').has_pact_with( 61 | Provider('UserService'), host_name=PACT_MOCK_HOST, port=PACT_MOCK_PORT, 62 | pact_dir=PACT_DIR) 63 | pact.start_service() 64 | yield pact 65 | pact.stop_service() 66 | 67 | version = request.config.getoption('--publish-pact') 68 | if not request.node.testsfailed and version: 69 | push_to_broker(version) 70 | 71 | 72 | def test_get_user_non_admin(pact, client): 73 | expected = { 74 | 'name': 'UserA', 75 | 'id': Term( 76 | r'^[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}\Z', 77 | '00000000-0000-4000-a000-000000000000' 78 | ), 79 | 'created_on': Term( 80 | r'\d+-\d+-\d+T\d+:\d+:\d+', 81 | '2016-12-15T20:16:01' 82 | ), 83 | 'admin': False 84 | } 85 | 86 | (pact 87 | .given('UserA exists and is not an administrator') 88 | .upon_receiving('a request for UserA') 89 | .with_request('get', '/users/UserA') 90 | .will_respond_with(200, body=Like(expected))) 91 | 92 | with pact: 93 | result = client.get_user('UserA') 94 | 95 | # assert something with the result, for ex, did I process 'result' properly? 96 | # or was I able to deserialize correctly? etc. 97 | 98 | 99 | def test_get_non_existing_user(pact, client): 100 | (pact 101 | .given('UserA does not exist') 102 | .upon_receiving('a request for UserA') 103 | .with_request('get', '/users/UserA') 104 | .will_respond_with(404)) 105 | 106 | with pact: 107 | result = client.get_user('UserA') 108 | 109 | assert result is None 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pact-python demo 2 | ================ 3 | 4 | This simple client/server implementation demos how pact-python can be used for a contract test. 5 | 6 | Further reading about pact is at https://docs.pact.io/ or https://docs.pact.io/blogs-videos-and-articles 7 | 8 | It has 3 components: 9 | * `pact_python_demo/user-app.py` -- a simple flask app that has a REST endpoint for `/users/` which returns a JSON representation of a user 10 | * `pact_python_demo/client.py` -- a simple client that gets a user from user-app. 11 | * `tests/test_client.py` -- a set of test cases using pytest and pact-python to test a simple 12 | contract between the client and server. 13 | * `broker/` -- contains docker-compose files for a pact broker server 14 | 15 | Set up your virtual environment with: 16 | 17 | ``` 18 | $ pip install pipenv 19 | $ pipenv install 20 | ``` 21 | 22 | ## Creating/validating pacts without the broker 23 | 24 | Enter your venv shell with: 25 | ``` 26 | $ pipenv shell 27 | ``` 28 | 29 | Run the test with: 30 | ``` 31 | (venv) $ pytest 32 | ``` 33 | 34 | You'll see a pact file is generated by this consumer test at `tests/userserviceclient-userservice.json` 35 | 36 | Then, fire up your server-side app and verify the provider works as expected: 37 | ``` 38 | (venv) $ python pact_python_demo/user-app.py 39 | (venv) $ pact-verifier --provider-base-url=http://localhost:5001 \ 40 | --pact-url=tests/userserviceclient-userservice.json \ 41 | --provider-states-setup-url=http://localhost:5001/_pact/provider_states 42 | ``` 43 | 44 | ## Creating/validating pacts and publishing results to the pact broker 45 | Next, you can try incorporating the pact broker in this process: 46 | 47 | Start the broker server: 48 | ``` 49 | $ cd broker 50 | $ docker-compose up 51 | ``` 52 | It's accessible at http://127.0.0.1 with username 'pactbroker' and password 'pactbroker' 53 | 54 | 55 | To run the test, this time pushing the generated pact into the broker with version '0.1' of UserServiceClient, use: 56 | ``` 57 | (venv) $ pytest --publish-pact 0.1 58 | ``` 59 | 60 | The `tests/conftest.py` adds this custom option, and `tests/test_client` has code in the `pact` fixture to upload to the broker if the command line option was passed. 61 | 62 | 63 | Then, you can validate the provider (UserService) and push the result to the broker by running: 64 | ``` 65 | (venv) $ python pact_python_demo/user-app.py 66 | (venv) ./verify_pact.sh 0.2 67 | ``` 68 | 69 | This runs the same `pact-verifier` command as above but has additional args for pulling the pact file from the broker (instead of getting the .json locally) and pushing the verification result to the broker. 70 | 71 | Log in to the broker and take a look at the matrix. You will see that UserClientService version 0.1 has been verified against UserService version 0.2 72 | 73 | 74 | How does this work? 75 | =================== 76 | 77 | The purpose of `test_client.py` is to simply verify that **if the server sends me what I'm expecting, MY client code behaves properly**. It is essentially a unit test to exercise your client code using a mocked server. Except in this case, the mock server is now a "pact mock provider". In the test, you have configured the mock provider server to respond to your client with certain data. This is the mock data to represent how you'd expect the provider to behave **given the proper state exists on the provider.** Using that mock data, you verify your client-side code works. 78 | 79 | Two tests were created to verify that my client behaves properly if: a) I get a 200OK response back w/ the json I'd expect to see, or b) I get a 404 back and I should be returning `None` 80 | 81 | For example, `client` expects that if `UserA` exists and it sends a GET to `/users/UserA`, it will get back JSON about UserA that has a name, uuid, timestamp for when the user was created, and a boolean indicating if the users is an admin or not. If the server responds with a 404, user client should return `None`. 82 | 83 | When your tests pass, a json **pact file** is generated. The `user-app` team can then take this pact file, and use it to verify that their service responds to requests in the way your client expects. 84 | 85 | ## Provider states 86 | 87 | A key part of this is **provider states**: 88 | * `client.py` expects a 200OK response with json about 'UserA' **given that userA exists on the provider** 89 | * In order for pact to verify the provider, `user-app.py` has to implement ways of **setting up the needed state**. This is done via a `provider-setup-url` which may reside on the provider app itself, or it could reside somewhere else (for example, some other test service you have setup that lives outside your provider app to populate your backend DB) 90 | * In this demo, the user app has provided another REST endpoint at `/_pact/provider_states` which the `pact-verifier` will send POST requests to. Depending on the state the pact-verifier asks for, the provider will need to set itself up (e.g. make DB changes, or do other things) to get itself into "the right state" before the verifier sends the "request under test" to it. 91 | 92 | ## Isn't my server gonna send back different stuff? 93 | Yes. The real server may respond with a timestamp or uuid that does not necessarily look like the mock values you created in your tests. Therefore, the `Term` object is used to say that **the server could send me anything that looks like this regex and that would be good for me, but for the purpose of this test I am using \**. 94 | 95 | The generated pact file used to verify the provider will only say "you need to be sending me data that looks like this regex" for timestamp and id. But, my client is not going to let server return with `name=UserB` if it asked for `name=UserA`, so the pact file is essentially saying "given that User A exists, and I send you a GET for userA, `name` in the JSON should be UserA" 96 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "8868207458f6caabcc2f42d9b7b6a6c9e706533974c1778db9ac2a7e318a6ff3" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "2.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "atomicwrites": { 20 | "hashes": [ 21 | "sha256:240831ea22da9ab882b551b31d4225591e5e447a68c5e188db5b89ca1d487585", 22 | "sha256:a24da68318b08ac9c9c45029f4a10371ab5b20e4226738e150e6e7c571630ae6" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.1.5" 26 | }, 27 | "attrs": { 28 | "hashes": [ 29 | "sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265", 30 | "sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b" 31 | ], 32 | "index": "pypi", 33 | "version": "==18.1.0" 34 | }, 35 | "certifi": { 36 | "hashes": [ 37 | "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7", 38 | "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0" 39 | ], 40 | "index": "pypi", 41 | "version": "==2018.4.16" 42 | }, 43 | "chardet": { 44 | "hashes": [ 45 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 46 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 47 | ], 48 | "index": "pypi", 49 | "version": "==3.0.4" 50 | }, 51 | "click": { 52 | "hashes": [ 53 | "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", 54 | "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" 55 | ], 56 | "index": "pypi", 57 | "version": "==6.7" 58 | }, 59 | "flask": { 60 | "hashes": [ 61 | "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", 62 | "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" 63 | ], 64 | "index": "pypi", 65 | "version": "==1.0.2" 66 | }, 67 | "funcsigs": { 68 | "hashes": [ 69 | "sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca", 70 | "sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50" 71 | ], 72 | "markers": "python_version < '3.0'", 73 | "version": "==1.0.2" 74 | }, 75 | "idna": { 76 | "hashes": [ 77 | "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", 78 | "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" 79 | ], 80 | "index": "pypi", 81 | "version": "==2.7" 82 | }, 83 | "itsdangerous": { 84 | "hashes": [ 85 | "sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519" 86 | ], 87 | "version": "==0.24" 88 | }, 89 | "jinja2": { 90 | "hashes": [ 91 | "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", 92 | "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" 93 | ], 94 | "version": "==2.10" 95 | }, 96 | "markupsafe": { 97 | "hashes": [ 98 | "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665" 99 | ], 100 | "version": "==1.0" 101 | }, 102 | "more-itertools": { 103 | "hashes": [ 104 | "sha256:2b6b9893337bfd9166bee6a62c2b0c9fe7735dcf85948b387ec8cba30e85d8e8", 105 | "sha256:6703844a52d3588f951883005efcf555e49566a48afd4db4e965d69b883980d3", 106 | "sha256:a18d870ef2ffca2b8463c0070ad17b5978056f403fb64e3f15fe62a52db21cc0" 107 | ], 108 | "index": "pypi", 109 | "version": "==4.2.0" 110 | }, 111 | "pact-python": { 112 | "hashes": [ 113 | "sha256:4107fb92e39a2e917a879d9bfe4ab117e898c73738f6dba4fc8fddc4074c5300" 114 | ], 115 | "index": "pypi", 116 | "version": "==0.16.1" 117 | }, 118 | "pipenv": { 119 | "hashes": [ 120 | "sha256:6ecf60c66187c6ca25a2dcd095036aba07539354a5011d94805fbfc5932582b0", 121 | "sha256:bb6bd074f853d9bab675942226a785a64d4fc42b5847538755e9573f5b77f63a", 122 | "sha256:d4b1aea904ae1488b7060137059642246baae709232f4536ad723ce216e4b540" 123 | ], 124 | "index": "pypi", 125 | "version": "==2018.7.1" 126 | }, 127 | "pluggy": { 128 | "hashes": [ 129 | "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff", 130 | "sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c", 131 | "sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5" 132 | ], 133 | "index": "pypi", 134 | "version": "==0.6.0" 135 | }, 136 | "psutil": { 137 | "hashes": [ 138 | "sha256:0ff2b16e9045d01edb1dd10d7fbcc184012e37f6cd38029e959f2be9c6223f50", 139 | "sha256:254adb6a27c888f141d2a6032ae231d8ed4fc5f7583b4c825e5f7d7c78d26d2e", 140 | "sha256:319e12f6bae4d4d988fbff3bed792953fa3b44c791f085b0a1a230f755671ef7", 141 | "sha256:529ae235896efb99a6f77653a7138273ab701ec9f0343a1f5030945108dee3c4", 142 | "sha256:686e5a35fe4c0acc25f3466c32e716f2d498aaae7b7edc03e2305b682226bcf6", 143 | "sha256:6d981b4d863b20c8ceed98b8ac3d1ca7f96d28707a80845d360fa69c8fc2c44b", 144 | "sha256:7789885a72aa3075d28d028236eb3f2b84d908f81d38ad41769a6ddc2fd81b7c", 145 | "sha256:7f4616bcb44a6afda930cfc40215e5e9fa7c6896e683b287c771c937712fbe2f", 146 | "sha256:7fdb3d02bfd68f508e6745021311a4a4dbfec53fca03721474e985f310e249ba", 147 | "sha256:a9b85b335b40a528a8e2a6b549592138de8429c6296e7361892958956e6a73cf", 148 | "sha256:dc85fad15ef98103ecc047a0d81b55bbf5fe1b03313b96e883acc2e2fa87ed5c" 149 | ], 150 | "index": "pypi", 151 | "version": "==5.4.6" 152 | }, 153 | "py": { 154 | "hashes": [ 155 | "sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7", 156 | "sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e" 157 | ], 158 | "index": "pypi", 159 | "version": "==1.5.4" 160 | }, 161 | "pytest": { 162 | "hashes": [ 163 | "sha256:0453c8676c2bee6feb0434748b068d5510273a916295fd61d306c4f22fbfd752", 164 | "sha256:4b208614ae6d98195430ad6bde03641c78553acee7c83cec2e85d613c0cd383d" 165 | ], 166 | "index": "pypi", 167 | "version": "==3.6.3" 168 | }, 169 | "requests": { 170 | "hashes": [ 171 | "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", 172 | "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a" 173 | ], 174 | "index": "pypi", 175 | "version": "==2.19.1" 176 | }, 177 | "six": { 178 | "hashes": [ 179 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", 180 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" 181 | ], 182 | "index": "pypi", 183 | "version": "==1.11.0" 184 | }, 185 | "subprocess32": { 186 | "hashes": [ 187 | "sha256:24b66882f5f7aaedcd46b4669322e88d639d46c6ce4679ae7f397bbe4fe9a529", 188 | "sha256:eb2b989cf03ffc7166339eb34f1aa26c5ace255243337b1e22dab7caa1166687" 189 | ], 190 | "version": "==3.5.2" 191 | }, 192 | "urllib3": { 193 | "hashes": [ 194 | "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", 195 | "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" 196 | ], 197 | "index": "pypi", 198 | "version": "==1.23" 199 | }, 200 | "virtualenv": { 201 | "hashes": [ 202 | "sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669", 203 | "sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752" 204 | ], 205 | "index": "pypi", 206 | "version": "==16.0.0" 207 | }, 208 | "virtualenv-clone": { 209 | "hashes": [ 210 | "sha256:4507071d81013fd03ea9930ec26bc8648b997927a11fa80e8ee81198b57e0ac7", 211 | "sha256:b5cfe535d14dc68dfc1d1bb4ac1209ea28235b91156e2bba8e250d291c3fb4f8" 212 | ], 213 | "index": "pypi", 214 | "version": "==0.3.0" 215 | }, 216 | "werkzeug": { 217 | "hashes": [ 218 | "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", 219 | "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" 220 | ], 221 | "version": "==0.14.1" 222 | } 223 | }, 224 | "develop": {} 225 | } 226 | --------------------------------------------------------------------------------