From ac1624808a5a744a1ee752c7f2693d6aa4643be8 Mon Sep 17 00:00:00 2001 From: Guido Barbaglia Date: Tue, 2 May 2017 13:50:24 +1000 Subject: [PATCH 01/85] Rename stuff. --- .gitignore | 2 ++ .travis.yml | 4 ++-- pytest_pact/PyPact.py => pact.py | 2 ++ pytest_pact/__init__.py | 0 requirements.txt | 3 ++- setup.py | 4 ++-- tests/test_books_service.py | 2 +- tests/test_pypact.py | 2 +- 8 files changed, 12 insertions(+), 7 deletions(-) rename pytest_pact/PyPact.py => pact.py (97%) delete mode 100644 pytest_pact/__init__.py diff --git a/.gitignore b/.gitignore index d254fc4..baf665e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ tests/__pycache__/ *.iml pytest_pact.egg-info/ pytest_pact/__pycache__ +__pycache__ +.DS_Store diff --git a/.travis.yml b/.travis.yml index 5bfd00c..aaa9282 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,10 @@ python: - 3.6 install: - "pip install -r requirements.txt" - - "pip install pytest pytest-cov pytest-sugar" + - "pip install pytest pytest-cov" - "pip install coveralls" - "pip install -e ." script: - - py.test --cov pytest_pact --cov-report term-missing + - py.test --cov pact --cov-report term-missing after_success: - coveralls diff --git a/pytest_pact/PyPact.py b/pact.py similarity index 97% rename from pytest_pact/PyPact.py rename to pact.py index f23d6da..0503b08 100644 --- a/pytest_pact/PyPact.py +++ b/pact.py @@ -15,6 +15,7 @@ @pytest.hookimpl(hookwrapper=True) def pytest_pyfunc_call(pyfuncitem): + print('SO???') # print(describe_consumer_pact(pyfuncitem)) # pypact = PyPact() @@ -42,6 +43,7 @@ class PyPactProvider(object): def describe_consumer_pact(pyfuncitem): + print('HALLO???') s = '' s += 'Given ' + read_marker(pyfuncitem, 'given') + ', ' s += 'upon receiving ' + read_marker(pyfuncitem, 'upon_receiving') + ' ' diff --git a/pytest_pact/__init__.py b/pytest_pact/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/requirements.txt b/requirements.txt index aa815b2..18fc441 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -pytest>=3.0 \ No newline at end of file +pytest>=3.0 +pytest-runner diff --git a/setup.py b/setup.py index 7bda334..dc6a2c8 100644 --- a/setup.py +++ b/setup.py @@ -12,11 +12,11 @@ description='Python implementation for Pact (http://pact.io/)', install_requires=[], setup_requires=['pytest-runner'], - tests_require=['pytest', 'pytest-sugar'], + tests_require=['pytest'], url='https://github.com/Kalimaha/pytest-pact/', entry_points = { 'pytest11': [ - 'pytest-pact = pytest_pact.PyPact', + 'pytest-pact = pact', ] } ) diff --git a/tests/test_books_service.py b/tests/test_books_service.py index fc4b6ee..3784dc8 100644 --- a/tests/test_books_service.py +++ b/tests/test_books_service.py @@ -1,4 +1,4 @@ -from pytest_pact.PyPact import * +from pact import * @base_uri('localhost:1234') diff --git a/tests/test_pypact.py b/tests/test_pypact.py index 5046f60..bca942c 100644 --- a/tests/test_pypact.py +++ b/tests/test_pypact.py @@ -1,4 +1,4 @@ -from pytest_pact.PyPact import * +from pact import * def test_read_existing_marker(): From 343f4f443930971d894adbd37ea11c56b0bd25ea Mon Sep 17 00:00:00 2001 From: Guido Barbaglia Date: Tue, 2 May 2017 15:59:06 +1000 Subject: [PATCH 02/85] Consumer or Provider. --- .travis.yml | 2 +- pact.py | 75 +++++++++++++++++++------------------ setup.cfg | 3 ++ setup.py | 4 +- tests/test_books_service.py | 46 +++++++++++------------ tests/test_pypact.py | 43 ++++++++++++++++++++- 6 files changed, 110 insertions(+), 63 deletions(-) diff --git a/.travis.yml b/.travis.yml index aaa9282..818e2d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: - 3.6 install: - "pip install -r requirements.txt" - - "pip install pytest pytest-cov" + - "pip install pytest pytest-cov pytest-pep8 pytest-sugar" - "pip install coveralls" - "pip install -e ." script: diff --git a/pact.py b/pact.py index 0503b08..ee3898b 100644 --- a/pact.py +++ b/pact.py @@ -1,31 +1,13 @@ import pytest -state = pytest.mark.state -given = pytest.mark.given -base_uri = pytest.mark.base_uri -pact_uri = pytest.mark.pact_uri -with_request = pytest.mark.with_request -has_pact_with = pytest.mark.has_pact_with -upon_receiving = pytest.mark.upon_receiving -will_respond_with = pytest.mark.will_respond_with -service_consumer = pytest.mark.service_consumer -honours_pact_with = pytest.mark.honours_pact_with - - @pytest.hookimpl(hookwrapper=True) def pytest_pyfunc_call(pyfuncitem): - print('SO???') - # print(describe_consumer_pact(pyfuncitem)) - - # pypact = PyPact() - # pypact.executor(pyfuncitem) - - + # print('SO???') outcome = yield # outcome.excinfo may be None or a (cls, val, tb) tuple - print(outcome) + # print(outcome) res = outcome.get_result() # will raise if outcome was exception # postprocess result @@ -33,26 +15,47 @@ def pytest_pyfunc_call(pyfuncitem): def consumer_or_provider(pyfuncitem): - pass + if is_consumer(pyfuncitem): + return CONSUMER + elif is_provider(pyfuncitem): + return PROVIDER + return None -class PyPactConsumer(object): - pass -class PyPactProvider(object): - pass +def is_consumer(pyfuncitem): + return read_marker(pyfuncitem, HAS_PACT_WITH) is not None -def describe_consumer_pact(pyfuncitem): - print('HALLO???') - s = '' - s += 'Given ' + read_marker(pyfuncitem, 'given') + ', ' - s += 'upon receiving ' + read_marker(pyfuncitem, 'upon_receiving') + ' ' - s += 'from ' + read_marker(pyfuncitem, 'service_consumer') + ' ' - s += 'with:\n\n' + read_marker(pyfuncitem, 'with_request') + '\n\n' - s += read_marker(pyfuncitem, 'has_pact_with') + ' will respond with:\n\n' - s += read_marker(pyfuncitem, 'will_respond_with') - return s +def is_provider(pyfuncitem): + return read_marker(pyfuncitem, HONOURS_PACT_WITH) is not None + def read_marker(pyfuncitem, marker_name): marker = pyfuncitem.get_marker(marker_name) - return str(marker.args[0]) if marker else '' + return str(marker.args[0]) if marker else None + + +state = pytest.mark.state +given = pytest.mark.given +base_uri = pytest.mark.base_uri +pact_uri = pytest.mark.pact_uri +with_request = pytest.mark.with_request +has_pact_with = pytest.mark.has_pact_with +upon_receiving = pytest.mark.upon_receiving +will_respond_with = pytest.mark.will_respond_with +service_consumer = pytest.mark.service_consumer +honours_pact_with = pytest.mark.honours_pact_with + + +CONSUMER = 'CONSUMER' +PROVIDER = 'PROVIDER' +HAS_PACT_WITH = 'has_pact_with' +HONOURS_PACT_WITH = 'honours_pact_with' +STATE = 'state' +GIVEN = 'given' +BASE_URI = 'base_uri' +PACT_URI = 'pact_uri' +WITH_REQUEST = 'with_request' +UPON_RECEIVING = 'upon_receiving' +WILL_RESPOND_WITH = 'will_respond_with' +SERVICE_CONSUMER = 'service_consumer' diff --git a/setup.cfg b/setup.cfg index b7e4789..58815e6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,5 @@ [aliases] test=pytest + +[tool:pytest] +addopts = --pep8 diff --git a/setup.py b/setup.py index dc6a2c8..80ac86f 100644 --- a/setup.py +++ b/setup.py @@ -12,9 +12,9 @@ description='Python implementation for Pact (http://pact.io/)', install_requires=[], setup_requires=['pytest-runner'], - tests_require=['pytest'], + tests_require=['pytest', 'pytest-pep8', 'pytest-sugar'], url='https://github.com/Kalimaha/pytest-pact/', - entry_points = { + entry_points={ 'pytest11': [ 'pytest-pact = pact', ] diff --git a/tests/test_books_service.py b/tests/test_books_service.py index 3784dc8..ee300e3 100644 --- a/tests/test_books_service.py +++ b/tests/test_books_service.py @@ -1,23 +1,23 @@ -from pact import * - - -@base_uri('localhost:1234') -@service_consumer('Library App') -@has_pact_with('Books Service') -class TestBooksService(): - - expected_response = { - 'status': 200, - 'headers': {'Content-Type': 'application/json'}, - 'body': { - 'id': '123', - 'title': 'A Fortune-Teller Told Me' - } - } - - @given('some books exist') - @upon_receiving('a request for a book') - @with_request({'method': 'get', 'path': '/books/123'}) - @will_respond_with(expected_response) - def test_get_book(self): - pass +# from pact import * +# +# +# @base_uri('localhost:1234') +# @service_consumer('Library App') +# @has_pact_with('Books Service') +# class TestBooksService(): +# +# expected_response = { +# 'status': 200, +# 'headers': {'Content-Type': 'application/json'}, +# 'body': { +# 'id': '123', +# 'title': 'A Fortune-Teller Told Me' +# } +# } +# +# @given('some books exist') +# @upon_receiving('a request for a book') +# @with_request({'method': 'get', 'path': '/books/123'}) +# @will_respond_with(expected_response) +# def test_get_book(self): +# pass diff --git a/tests/test_pypact.py b/tests/test_pypact.py index bca942c..fcc7405 100644 --- a/tests/test_pypact.py +++ b/tests/test_pypact.py @@ -3,20 +3,61 @@ def test_read_existing_marker(): expected_marker = 'some books exist' + class FakeMarker(object): args = [expected_marker] + class FakePyFuncItem(object): def get_marker(self, marker_name): return FakeMarker() + pyfuncitem = FakePyFuncItem() marker_name = 'given' assert read_marker(pyfuncitem, marker_name) == expected_marker def test_read_non_existing_marker(): + class FakePyFuncItem(object): def get_marker(self, marker_name): return None + pyfuncitem = FakePyFuncItem() marker_name = 'given' - assert read_marker(pyfuncitem, marker_name) == '' + assert read_marker(pyfuncitem, marker_name) is None + + +def test_is_standard_test(): + + class FakePyFuncItem(object): + def get_marker(self, marker_name): + return None + + pyfuncitem = FakePyFuncItem() + assert consumer_or_provider(pyfuncitem) is None + + +def test_is_consumer_test(): + + class FakeMarker(object): + args = ['Books Service'] + + class FakePyFuncItem(object): + def get_marker(self, marker_name): + return FakeMarker() if marker_name == HAS_PACT_WITH else None + + pyfuncitem = FakePyFuncItem() + assert consumer_or_provider(pyfuncitem) == CONSUMER + + +def test_is_provider_test(): + + class FakeMarker(object): + args = ['Library App'] + + class FakePyFuncItem(object): + def get_marker(self, marker_name): + return FakeMarker() if marker_name == HONOURS_PACT_WITH else None + + pyfuncitem = FakePyFuncItem() + assert consumer_or_provider(pyfuncitem) == PROVIDER From 4d67253ae955aba76b68662119c5f6eef1b802a4 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 13 May 2017 12:28:36 +1000 Subject: [PATCH 03/85] Provider test executor and validation. --- .travis.yml | 7 ++- README.md | 10 ----- README.rst | 18 ++++++++ pact.py | 62 +++------------------------ pytest_pact/__init__.py | 0 pytest_pact/consumer_executor.py | 9 ++++ pytest_pact/executor.py | 24 +++++++++++ pytest_pact/pact_markers.py | 26 ++++++++++++ pytest_pact/pact_utils.py | 37 ++++++++++++++++ pytest_pact/provider_executor.py | 14 ++++++ pytest_pact/pytest_utils.py | 6 +++ pytest_pact/standard_executor.py | 5 +++ setup.py | 2 +- tests/test_books_service.py | 46 ++++++++++---------- tests/test_pact_utils.py | 73 ++++++++++++++++++++++++++++++++ tests/test_provider_executor.py | 37 ++++++++++++++++ tests/test_pypact.py | 62 --------------------------- tests/test_pytest_utils.py | 27 ++++++++++++ 18 files changed, 310 insertions(+), 155 deletions(-) delete mode 100644 README.md create mode 100644 README.rst create mode 100644 pytest_pact/__init__.py create mode 100644 pytest_pact/consumer_executor.py create mode 100644 pytest_pact/executor.py create mode 100644 pytest_pact/pact_markers.py create mode 100644 pytest_pact/pact_utils.py create mode 100644 pytest_pact/provider_executor.py create mode 100644 pytest_pact/pytest_utils.py create mode 100644 pytest_pact/standard_executor.py create mode 100644 tests/test_pact_utils.py create mode 100644 tests/test_provider_executor.py create mode 100644 tests/test_pytest_utils.py diff --git a/.travis.yml b/.travis.yml index 818e2d3..9bdc184 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,9 @@ python: - 3.6 install: - "pip install -r requirements.txt" - - "pip install pytest pytest-cov pytest-pep8 pytest-sugar" - - "pip install coveralls" + - "pip install pytest pytest-cov pytest-pep8 pytest-sugar codecov" - "pip install -e ." script: - - py.test --cov pact --cov-report term-missing + - py.test --cov=./ after_success: - - coveralls + - codecov diff --git a/README.md b/README.md deleted file mode 100644 index d755fb3..0000000 --- a/README.md +++ /dev/null @@ -1,10 +0,0 @@ -[![Build Status](https://travis-ci.org/Kalimaha/pytest-pact.svg?branch=master)](https://travis-ci.org/Kalimaha/pytest-pact) -[![Coverage Status](https://coveralls.io/repos/github/Kalimaha/pytest-pact/badge.svg?branch=master)](https://coveralls.io/github/Kalimaha/pytest-pact?branch=master) - -# PyPact -Python implementation for Pact (http://pact.io/) - -## Setup -``` -python setup.py install -``` diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..4c31493 --- /dev/null +++ b/README.rst @@ -0,0 +1,18 @@ +.. image:: https://travis-ci.org/Kalimaha/pytest-pact.svg?branch=master + :target: https://travis-ci.org/Kalimaha/pytest-pact +.. image:: https://codecov.io/gh/Kalimaha/pytest-pact/branch/master/graph/badge.svg + :target: https://codecov.io/gh/Kalimaha/pytest-pact +.. image:: https://img.shields.io/badge/python-2.7-blue.svg +.. image:: https://img.shields.io/badge/python-3.3-blue.svg +.. image:: https://img.shields.io/badge/python-3.4-blue.svg +.. image:: https://img.shields.io/badge/python-3.5-blue.svg +.. image:: https://img.shields.io/badge/python-3.6-blue.svg + +Pact for PyTest +=============== +Python implementation for Pact (http://pact.io/) + +Setup +----- + + python setup.py install diff --git a/pact.py b/pact.py index ee3898b..5592e5c 100644 --- a/pact.py +++ b/pact.py @@ -1,61 +1,13 @@ import pytest +from pytest_pact.pact_utils import * +from pytest_pact.pytest_utils import read_marker +from pytest_pact.pact_markers import * @pytest.hookimpl(hookwrapper=True) def pytest_pyfunc_call(pyfuncitem): - # print('SO???') - + test_executor = executor(pyfuncitem) + test_executor.set_up() outcome = yield - # outcome.excinfo may be None or a (cls, val, tb) tuple - # print(outcome) - - res = outcome.get_result() # will raise if outcome was exception - # postprocess result - print(res) - - -def consumer_or_provider(pyfuncitem): - if is_consumer(pyfuncitem): - return CONSUMER - elif is_provider(pyfuncitem): - return PROVIDER - return None - - -def is_consumer(pyfuncitem): - return read_marker(pyfuncitem, HAS_PACT_WITH) is not None - - -def is_provider(pyfuncitem): - return read_marker(pyfuncitem, HONOURS_PACT_WITH) is not None - - -def read_marker(pyfuncitem, marker_name): - marker = pyfuncitem.get_marker(marker_name) - return str(marker.args[0]) if marker else None - - -state = pytest.mark.state -given = pytest.mark.given -base_uri = pytest.mark.base_uri -pact_uri = pytest.mark.pact_uri -with_request = pytest.mark.with_request -has_pact_with = pytest.mark.has_pact_with -upon_receiving = pytest.mark.upon_receiving -will_respond_with = pytest.mark.will_respond_with -service_consumer = pytest.mark.service_consumer -honours_pact_with = pytest.mark.honours_pact_with - - -CONSUMER = 'CONSUMER' -PROVIDER = 'PROVIDER' -HAS_PACT_WITH = 'has_pact_with' -HONOURS_PACT_WITH = 'honours_pact_with' -STATE = 'state' -GIVEN = 'given' -BASE_URI = 'base_uri' -PACT_URI = 'pact_uri' -WITH_REQUEST = 'with_request' -UPON_RECEIVING = 'upon_receiving' -WILL_RESPOND_WITH = 'will_respond_with' -SERVICE_CONSUMER = 'service_consumer' + res = outcome.get_result() + test_executor.tear_down() diff --git a/pytest_pact/__init__.py b/pytest_pact/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pytest_pact/consumer_executor.py b/pytest_pact/consumer_executor.py new file mode 100644 index 0000000..a228a60 --- /dev/null +++ b/pytest_pact/consumer_executor.py @@ -0,0 +1,9 @@ +from pytest_pact.executor import Executor + + +class ConsumerExecutor(Executor): + def set_up(self): + pass + + def tear_down(self): + pass diff --git a/pytest_pact/executor.py b/pytest_pact/executor.py new file mode 100644 index 0000000..c1c36ae --- /dev/null +++ b/pytest_pact/executor.py @@ -0,0 +1,24 @@ +from pytest_pact.pytest_utils import read_marker + + +class Executor(object): + REQUIRED_PARAMETERS = [] + + def __init__(self, pyfuncitem): + self.pyfuncitem = pyfuncitem + + def set_up(self): + pass + + def tear_down(self): + pass + + def is_valid(self): + check = {} + for marker in self.REQUIRED_PARAMETERS: + marker_value = read_marker(self.pyfuncitem, marker) + if marker_value is not None: + check[marker] = marker_value + valid_keys = sorted(check.keys()) == sorted(self.REQUIRED_PARAMETERS) + valid_values = all(v is not None for v in check.values()) + return valid_keys and valid_values diff --git a/pytest_pact/pact_markers.py b/pytest_pact/pact_markers.py new file mode 100644 index 0000000..384aec4 --- /dev/null +++ b/pytest_pact/pact_markers.py @@ -0,0 +1,26 @@ +import pytest + + +state = pytest.mark.state +given = pytest.mark.given +base_uri = pytest.mark.base_uri +pact_uri = pytest.mark.pact_uri +with_request = pytest.mark.with_request +has_pact_with = pytest.mark.has_pact_with +upon_receiving = pytest.mark.upon_receiving +will_respond_with = pytest.mark.will_respond_with +service_consumer = pytest.mark.service_consumer +honours_pact_with = pytest.mark.honours_pact_with + +CONSUMER = 'CONSUMER' +PROVIDER = 'PROVIDER' +HAS_PACT_WITH = 'has_pact_with' +HONOURS_PACT_WITH = 'honours_pact_with' +STATE = 'state' +GIVEN = 'given' +BASE_URI = 'base_uri' +PACT_URI = 'pact_uri' +WITH_REQUEST = 'with_request' +UPON_RECEIVING = 'upon_receiving' +WILL_RESPOND_WITH = 'will_respond_with' +SERVICE_CONSUMER = 'service_consumer' diff --git a/pytest_pact/pact_utils.py b/pytest_pact/pact_utils.py new file mode 100644 index 0000000..e6aaa18 --- /dev/null +++ b/pytest_pact/pact_utils.py @@ -0,0 +1,37 @@ +import pytest +from pytest_pact.pytest_utils import read_marker +from pytest_pact.pact_markers import CONSUMER +from pytest_pact.pact_markers import PROVIDER +from pytest_pact.pact_markers import HAS_PACT_WITH +from pytest_pact.pact_markers import HONOURS_PACT_WITH +from pytest_pact.standard_executor import StandardExecutor +from pytest_pact.consumer_executor import ConsumerExecutor +from pytest_pact.provider_executor import ProviderExecutor + + +def executor(pyfuncitem): + executor_type = pact_type(pyfuncitem) + executor = None + if executor_type is CONSUMER: + executor = ConsumerExecutor(pyfuncitem) + elif executor_type is PROVIDER: + executor = ProviderExecutor(pyfuncitem) + else: + executor = StandardExecutor(pyfuncitem) + return executor + + +def pact_type(pyfuncitem): + if is_consumer(pyfuncitem): + return CONSUMER + elif is_provider(pyfuncitem): + return PROVIDER + return None + + +def is_consumer(pyfuncitem): + return read_marker(pyfuncitem, HAS_PACT_WITH) is not None + + +def is_provider(pyfuncitem): + return read_marker(pyfuncitem, HONOURS_PACT_WITH) is not None diff --git a/pytest_pact/provider_executor.py b/pytest_pact/provider_executor.py new file mode 100644 index 0000000..2a154c9 --- /dev/null +++ b/pytest_pact/provider_executor.py @@ -0,0 +1,14 @@ +from pytest_pact.executor import Executor +from pytest_pact.pact_markers import STATE +from pytest_pact.pact_markers import PACT_URI +from pytest_pact.pact_markers import HONOURS_PACT_WITH + + +class ProviderExecutor(Executor): + REQUIRED_PARAMETERS = [STATE, PACT_URI, HONOURS_PACT_WITH] + + def set_up(self): + pass + + def tear_down(self): + pass diff --git a/pytest_pact/pytest_utils.py b/pytest_pact/pytest_utils.py new file mode 100644 index 0000000..8fa32f5 --- /dev/null +++ b/pytest_pact/pytest_utils.py @@ -0,0 +1,6 @@ +import pytest + + +def read_marker(pyfuncitem, marker_name): + marker = pyfuncitem.get_marker(marker_name) + return marker.args[0] if marker else None diff --git a/pytest_pact/standard_executor.py b/pytest_pact/standard_executor.py new file mode 100644 index 0000000..5e8d1c6 --- /dev/null +++ b/pytest_pact/standard_executor.py @@ -0,0 +1,5 @@ +from pytest_pact.executor import Executor + + +class StandardExecutor(Executor): + pass diff --git a/setup.py b/setup.py index 80ac86f..f1e2b1a 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ author_email='guido.barbaglia@gmail.com', packages=find_packages(), license='LICENSE.txt', - long_description=open('README.md').read(), + long_description=open('README.rst').read(), description='Python implementation for Pact (http://pact.io/)', install_requires=[], setup_requires=['pytest-runner'], diff --git a/tests/test_books_service.py b/tests/test_books_service.py index ee300e3..3784dc8 100644 --- a/tests/test_books_service.py +++ b/tests/test_books_service.py @@ -1,23 +1,23 @@ -# from pact import * -# -# -# @base_uri('localhost:1234') -# @service_consumer('Library App') -# @has_pact_with('Books Service') -# class TestBooksService(): -# -# expected_response = { -# 'status': 200, -# 'headers': {'Content-Type': 'application/json'}, -# 'body': { -# 'id': '123', -# 'title': 'A Fortune-Teller Told Me' -# } -# } -# -# @given('some books exist') -# @upon_receiving('a request for a book') -# @with_request({'method': 'get', 'path': '/books/123'}) -# @will_respond_with(expected_response) -# def test_get_book(self): -# pass +from pact import * + + +@base_uri('localhost:1234') +@service_consumer('Library App') +@has_pact_with('Books Service') +class TestBooksService(): + + expected_response = { + 'status': 200, + 'headers': {'Content-Type': 'application/json'}, + 'body': { + 'id': '123', + 'title': 'A Fortune-Teller Told Me' + } + } + + @given('some books exist') + @upon_receiving('a request for a book') + @with_request({'method': 'get', 'path': '/books/123'}) + @will_respond_with(expected_response) + def test_get_book(self): + pass diff --git a/tests/test_pact_utils.py b/tests/test_pact_utils.py new file mode 100644 index 0000000..acf5232 --- /dev/null +++ b/tests/test_pact_utils.py @@ -0,0 +1,73 @@ +from pytest_pact.pact_utils import * + + +def test_standard_executor(): + + class FakePyFuncItem(object): + def get_marker(self, marker_name): + return None + + pyfuncitem = FakePyFuncItem() + assert type(executor(pyfuncitem)).__name__ is 'StandardExecutor' + + +def test_consumer_executor(): + + class FakeMarker(object): + args = ['Books Service'] + + class FakePyFuncItem(object): + def get_marker(self, marker_name): + return FakeMarker() if marker_name == HAS_PACT_WITH else None + + pyfuncitem = FakePyFuncItem() + assert type(executor(pyfuncitem)).__name__ is 'ConsumerExecutor' + + +def test_provider_executor(): + + class FakeMarker(object): + args = ['Library App'] + + class FakePyFuncItem(object): + def get_marker(self, marker_name): + return FakeMarker() if marker_name == HONOURS_PACT_WITH else None + + pyfuncitem = FakePyFuncItem() + assert type(executor(pyfuncitem)).__name__ is 'ProviderExecutor' + + +def test_is_standard_test(): + + class FakePyFuncItem(object): + def get_marker(self, marker_name): + return None + + pyfuncitem = FakePyFuncItem() + assert pact_type(pyfuncitem) is None + + +def test_is_consumer_test(): + + class FakeMarker(object): + args = ['Books Service'] + + class FakePyFuncItem(object): + def get_marker(self, marker_name): + return FakeMarker() if marker_name == HAS_PACT_WITH else None + + pyfuncitem = FakePyFuncItem() + assert pact_type(pyfuncitem) == CONSUMER + + +def test_is_provider_test(): + + class FakeMarker(object): + args = ['Library App'] + + class FakePyFuncItem(object): + def get_marker(self, marker_name): + return FakeMarker() if marker_name == HONOURS_PACT_WITH else None + + pyfuncitem = FakePyFuncItem() + assert pact_type(pyfuncitem) == PROVIDER diff --git a/tests/test_provider_executor.py b/tests/test_provider_executor.py new file mode 100644 index 0000000..9f7c403 --- /dev/null +++ b/tests/test_provider_executor.py @@ -0,0 +1,37 @@ +from pytest_pact.provider_executor import ProviderExecutor + + +def test_valid_setup(): + class FakeMarker(object): + def __init__(self, marker_name): + self.args = [marker_name + '_VALUE'] + + class FakePyFuncItem(object): + def get_marker(self, marker_name): + return FakeMarker(marker_name) + pyfuncitem = FakePyFuncItem() + assert ProviderExecutor(pyfuncitem).is_valid() + + +def test_missing_markers(): + class FakeMarker(object): + def __init__(self, marker_name): + self.args = [marker_name + '_VALUE'] + + class FakePyFuncItem(object): + def get_marker(self, marker_name): + return FakeMarker(marker_name) if marker_name is 'state' else None + pyfuncitem = FakePyFuncItem() + assert ProviderExecutor(pyfuncitem).is_valid() is False + + +def test_null_values(): + class FakeMarker(object): + def __init__(self, marker_name): + self.args = [42] if marker_name is 'state' else [None] + + class FakePyFuncItem(object): + def get_marker(self, marker_name): + return FakeMarker(marker_name) + pyfuncitem = FakePyFuncItem() + assert ProviderExecutor(pyfuncitem).is_valid() is False diff --git a/tests/test_pypact.py b/tests/test_pypact.py index fcc7405..e03f19a 100644 --- a/tests/test_pypact.py +++ b/tests/test_pypact.py @@ -1,63 +1 @@ from pact import * - - -def test_read_existing_marker(): - expected_marker = 'some books exist' - - class FakeMarker(object): - args = [expected_marker] - - class FakePyFuncItem(object): - def get_marker(self, marker_name): - return FakeMarker() - - pyfuncitem = FakePyFuncItem() - marker_name = 'given' - assert read_marker(pyfuncitem, marker_name) == expected_marker - - -def test_read_non_existing_marker(): - - class FakePyFuncItem(object): - def get_marker(self, marker_name): - return None - - pyfuncitem = FakePyFuncItem() - marker_name = 'given' - assert read_marker(pyfuncitem, marker_name) is None - - -def test_is_standard_test(): - - class FakePyFuncItem(object): - def get_marker(self, marker_name): - return None - - pyfuncitem = FakePyFuncItem() - assert consumer_or_provider(pyfuncitem) is None - - -def test_is_consumer_test(): - - class FakeMarker(object): - args = ['Books Service'] - - class FakePyFuncItem(object): - def get_marker(self, marker_name): - return FakeMarker() if marker_name == HAS_PACT_WITH else None - - pyfuncitem = FakePyFuncItem() - assert consumer_or_provider(pyfuncitem) == CONSUMER - - -def test_is_provider_test(): - - class FakeMarker(object): - args = ['Library App'] - - class FakePyFuncItem(object): - def get_marker(self, marker_name): - return FakeMarker() if marker_name == HONOURS_PACT_WITH else None - - pyfuncitem = FakePyFuncItem() - assert consumer_or_provider(pyfuncitem) == PROVIDER diff --git a/tests/test_pytest_utils.py b/tests/test_pytest_utils.py new file mode 100644 index 0000000..c2f47d8 --- /dev/null +++ b/tests/test_pytest_utils.py @@ -0,0 +1,27 @@ +from pytest_pact.pytest_utils import * + + +def test_read_existing_marker(): + expected_marker = 'some books exist' + + class FakeMarker(object): + args = [expected_marker] + + class FakePyFuncItem(object): + def get_marker(self, marker_name): + return FakeMarker() + + pyfuncitem = FakePyFuncItem() + marker_name = 'given' + assert read_marker(pyfuncitem, marker_name) == expected_marker + + +def test_read_non_existing_marker(): + + class FakePyFuncItem(object): + def get_marker(self, marker_name): + return None + + pyfuncitem = FakePyFuncItem() + marker_name = 'given' + assert read_marker(pyfuncitem, marker_name) is None From 0aa4c3ed0feefa68214be31801cd8a2084b74fe3 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 13 May 2017 15:36:09 +1000 Subject: [PATCH 04/85] Consumer executor and validation. --- README.rst | 5 +++++ pytest_pact/consumer_executor.py | 11 ++++++++++ tests/test_consumer_executor.py | 37 ++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 tests/test_consumer_executor.py diff --git a/README.rst b/README.rst index 4c31493..d63c53c 100644 --- a/README.rst +++ b/README.rst @@ -3,10 +3,15 @@ .. image:: https://codecov.io/gh/Kalimaha/pytest-pact/branch/master/graph/badge.svg :target: https://codecov.io/gh/Kalimaha/pytest-pact .. image:: https://img.shields.io/badge/python-2.7-blue.svg + :target: https://travis-ci.org/Kalimaha/pytest-pact .. image:: https://img.shields.io/badge/python-3.3-blue.svg + :target: https://travis-ci.org/Kalimaha/pytest-pact .. image:: https://img.shields.io/badge/python-3.4-blue.svg + :target: https://travis-ci.org/Kalimaha/pytest-pact .. image:: https://img.shields.io/badge/python-3.5-blue.svg + :target: https://travis-ci.org/Kalimaha/pytest-pact .. image:: https://img.shields.io/badge/python-3.6-blue.svg + :target: https://travis-ci.org/Kalimaha/pytest-pact Pact for PyTest =============== diff --git a/pytest_pact/consumer_executor.py b/pytest_pact/consumer_executor.py index a228a60..ce3fac0 100644 --- a/pytest_pact/consumer_executor.py +++ b/pytest_pact/consumer_executor.py @@ -1,7 +1,18 @@ from pytest_pact.executor import Executor +from pytest_pact.pact_markers import SERVICE_CONSUMER +from pytest_pact.pact_markers import HAS_PACT_WITH +from pytest_pact.pact_markers import BASE_URI +from pytest_pact.pact_markers import GIVEN +from pytest_pact.pact_markers import UPON_RECEIVING +from pytest_pact.pact_markers import WITH_REQUEST +from pytest_pact.pact_markers import WILL_RESPOND_WITH class ConsumerExecutor(Executor): + REQUIRED_PARAMETERS = [SERVICE_CONSUMER, HAS_PACT_WITH, BASE_URI, + GIVEN, UPON_RECEIVING, WITH_REQUEST, + WILL_RESPOND_WITH] + def set_up(self): pass diff --git a/tests/test_consumer_executor.py b/tests/test_consumer_executor.py new file mode 100644 index 0000000..a906600 --- /dev/null +++ b/tests/test_consumer_executor.py @@ -0,0 +1,37 @@ +from pytest_pact.consumer_executor import ConsumerExecutor + + +def test_valid_setup(): + class FakeMarker(object): + def __init__(self, marker_name): + self.args = [marker_name + '_VALUE'] + + class FakePyFuncItem(object): + def get_marker(self, marker_name): + return FakeMarker(marker_name) + pyfuncitem = FakePyFuncItem() + assert ConsumerExecutor(pyfuncitem).is_valid() + + +def test_missing_markers(): + class FakeMarker(object): + def __init__(self, marker_name): + self.args = [marker_name + '_VALUE'] + + class FakePyFuncItem(object): + def get_marker(self, marker_name): + return FakeMarker(marker_name) if marker_name is 'state' else None + pyfuncitem = FakePyFuncItem() + assert ConsumerExecutor(pyfuncitem).is_valid() is False + + +def test_null_values(): + class FakeMarker(object): + def __init__(self, marker_name): + self.args = [42] if marker_name is 'state' else [None] + + class FakePyFuncItem(object): + def get_marker(self, marker_name): + return FakeMarker(marker_name) + pyfuncitem = FakePyFuncItem() + assert ConsumerExecutor(pyfuncitem).is_valid() is False From 376ab9bfcd3a332807f7b3773085eb8081aba80a Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 13 May 2017 16:47:37 +1000 Subject: [PATCH 05/85] Provider executor Locate, load and validate pact_helper module. --- .travis.yml | 3 +- flask_service.py | 5 + pact.py | 6 +- pytest_pact/constants/__init__.py | 1 + pytest_pact/constants/constants.py | 10 ++ pytest_pact/executors/__init__.py | 0 .../{ => executors}/consumer_executor.py | 2 +- pytest_pact/{ => executors}/executor.py | 2 +- pytest_pact/executors/provider_executor.py | 37 +++++ pytest_pact/executors/standard_executor.py | 5 + pytest_pact/provider_executor.py | 14 -- pytest_pact/standard_executor.py | 5 - pytest_pact/utils/__init__.py | 0 pytest_pact/{ => utils}/pact_utils.py | 8 +- pytest_pact/{ => utils}/pytest_utils.py | 0 requirements.txt | 2 - setup.cfg | 5 + setup.py | 11 +- tests/executors/__init__.py | 0 .../{ => executors}/test_consumer_executor.py | 2 +- tests/executors/test_provider_executor.py | 129 ++++++++++++++++++ tests/resources/__init__.py | 0 tests/resources/pact_helper.py | 6 + tests/resources/pact_helper_no_setup.py | 2 + tests/resources/pact_helper_no_teardown.py | 2 + tests/test_books_service.py | 23 ---- tests/test_pact_utils.py | 73 ---------- tests/test_provider_executor.py | 37 ----- tests/test_pypact.py | 1 - tests/utils/__init__.py | 0 tests/utils/test_pact_utils.py | 82 +++++++++++ tests/{ => utils}/test_pytest_utils.py | 12 +- 32 files changed, 303 insertions(+), 182 deletions(-) create mode 100644 flask_service.py create mode 100644 pytest_pact/constants/__init__.py create mode 100644 pytest_pact/constants/constants.py create mode 100644 pytest_pact/executors/__init__.py rename pytest_pact/{ => executors}/consumer_executor.py (92%) rename pytest_pact/{ => executors}/executor.py (92%) create mode 100644 pytest_pact/executors/provider_executor.py create mode 100644 pytest_pact/executors/standard_executor.py delete mode 100644 pytest_pact/provider_executor.py delete mode 100644 pytest_pact/standard_executor.py create mode 100644 pytest_pact/utils/__init__.py rename pytest_pact/{ => utils}/pact_utils.py (77%) rename pytest_pact/{ => utils}/pytest_utils.py (100%) delete mode 100644 requirements.txt create mode 100644 tests/executors/__init__.py rename tests/{ => executors}/test_consumer_executor.py (94%) create mode 100644 tests/executors/test_provider_executor.py create mode 100644 tests/resources/__init__.py create mode 100644 tests/resources/pact_helper.py create mode 100644 tests/resources/pact_helper_no_setup.py create mode 100644 tests/resources/pact_helper_no_teardown.py delete mode 100644 tests/test_books_service.py delete mode 100644 tests/test_pact_utils.py delete mode 100644 tests/test_provider_executor.py delete mode 100644 tests/test_pypact.py create mode 100644 tests/utils/__init__.py create mode 100644 tests/utils/test_pact_utils.py rename tests/{ => utils}/test_pytest_utils.py (57%) diff --git a/.travis.yml b/.travis.yml index 9bdc184..23647a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,7 @@ python: - 3.5 - 3.6 install: - - "pip install -r requirements.txt" - - "pip install pytest pytest-cov pytest-pep8 pytest-sugar codecov" + - "pip install pytest pytest-cov pytest-pep8 pytest-sugar pytest-mock codecov" - "pip install -e ." script: - py.test --cov=./ diff --git a/flask_service.py b/flask_service.py new file mode 100644 index 0000000..f5574e5 --- /dev/null +++ b/flask_service.py @@ -0,0 +1,5 @@ +from flask import Flask +from flask import request + + +app = Flask(__name__) diff --git a/pact.py b/pact.py index 5592e5c..b6cd84a 100644 --- a/pact.py +++ b/pact.py @@ -1,12 +1,14 @@ import pytest -from pytest_pact.pact_utils import * -from pytest_pact.pytest_utils import read_marker +from pytest_pact.utils.pact_utils import * +from pytest_pact.utils.pytest_utils import read_marker from pytest_pact.pact_markers import * @pytest.hookimpl(hookwrapper=True) def pytest_pyfunc_call(pyfuncitem): test_executor = executor(pyfuncitem) + print('=============================') + print(test_executor) test_executor.set_up() outcome = yield res = outcome.get_result() diff --git a/pytest_pact/constants/__init__.py b/pytest_pact/constants/__init__.py new file mode 100644 index 0000000..b40dbcb --- /dev/null +++ b/pytest_pact/constants/__init__.py @@ -0,0 +1 @@ +__all__ = ['constants'] diff --git a/pytest_pact/constants/constants.py b/pytest_pact/constants/constants.py new file mode 100644 index 0000000..d218474 --- /dev/null +++ b/pytest_pact/constants/constants.py @@ -0,0 +1,10 @@ +state = pytest.mark.state +given = pytest.mark.given +base_uri = pytest.mark.base_uri +pact_uri = pytest.mark.pact_uri +with_request = pytest.mark.with_request +has_pact_with = pytest.mark.has_pact_with +upon_receiving = pytest.mark.upon_receiving +will_respond_with = pytest.mark.will_respond_with +service_consumer = pytest.mark.service_consumer +honours_pact_with = pytest.mark.honours_pact_with diff --git a/pytest_pact/executors/__init__.py b/pytest_pact/executors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pytest_pact/consumer_executor.py b/pytest_pact/executors/consumer_executor.py similarity index 92% rename from pytest_pact/consumer_executor.py rename to pytest_pact/executors/consumer_executor.py index ce3fac0..1537a95 100644 --- a/pytest_pact/consumer_executor.py +++ b/pytest_pact/executors/consumer_executor.py @@ -1,4 +1,4 @@ -from pytest_pact.executor import Executor +from pytest_pact.executors.executor import Executor from pytest_pact.pact_markers import SERVICE_CONSUMER from pytest_pact.pact_markers import HAS_PACT_WITH from pytest_pact.pact_markers import BASE_URI diff --git a/pytest_pact/executor.py b/pytest_pact/executors/executor.py similarity index 92% rename from pytest_pact/executor.py rename to pytest_pact/executors/executor.py index c1c36ae..88fbc69 100644 --- a/pytest_pact/executor.py +++ b/pytest_pact/executors/executor.py @@ -1,4 +1,4 @@ -from pytest_pact.pytest_utils import read_marker +from pytest_pact.utils.pytest_utils import read_marker class Executor(object): diff --git a/pytest_pact/executors/provider_executor.py b/pytest_pact/executors/provider_executor.py new file mode 100644 index 0000000..f30d153 --- /dev/null +++ b/pytest_pact/executors/provider_executor.py @@ -0,0 +1,37 @@ +import os +import sys +import imp +from pytest_pact.executors.executor import Executor +from pytest_pact.pact_markers import STATE +from pytest_pact.pact_markers import PACT_URI +from pytest_pact.pact_markers import HONOURS_PACT_WITH + + +class ProviderExecutor(Executor): + REQUIRED_PARAMETERS = [STATE, PACT_URI, HONOURS_PACT_WITH] + PACT_HELPER = 'pact_helper.py' + PACT_HELPER_NOT_FOUND = 'Could\'n find "pact_helper.py" script in Pact test directory.' + SET_UP_NOT_FOUND = 'Module pact_helper MUST have a set_up method' + TEAR_DOWN_NOT_FOUND = 'Module pact_helper MUST have a tear_down method' + pact_helper = None + + def set_up(self): + path_to_pact_helper = self.pact_helper_path() + self.pact_helper = self.load_pact_helper(path_to_pact_helper) + self.pact_helper.set_up() + + def tear_down(self): + self.pact_helper.tear_down() + + def load_pact_helper(self, path_to_pact_helper): + pact_helper = imp.load_source('pact_helper', path_to_pact_helper) + if hasattr(pact_helper, 'set_up') is False: raise Exception(self.SET_UP_NOT_FOUND) + if hasattr(pact_helper, 'tear_down') is False: raise Exception(self.TEAR_DOWN_NOT_FOUND) + return pact_helper + + def pact_helper_path(self): + test_dir = os.path.dirname(self.pyfuncitem.fspath) + files = [f for f in os.listdir(test_dir) if f == self.PACT_HELPER] + if not files: raise Exception(self.PACT_HELPER_NOT_FOUND) + pact_helper_path = os.path.join(test_dir, files[0]) + return pact_helper_path diff --git a/pytest_pact/executors/standard_executor.py b/pytest_pact/executors/standard_executor.py new file mode 100644 index 0000000..b4eddaa --- /dev/null +++ b/pytest_pact/executors/standard_executor.py @@ -0,0 +1,5 @@ +from pytest_pact.executors.executor import Executor + + +class StandardExecutor(Executor): + pass diff --git a/pytest_pact/provider_executor.py b/pytest_pact/provider_executor.py deleted file mode 100644 index 2a154c9..0000000 --- a/pytest_pact/provider_executor.py +++ /dev/null @@ -1,14 +0,0 @@ -from pytest_pact.executor import Executor -from pytest_pact.pact_markers import STATE -from pytest_pact.pact_markers import PACT_URI -from pytest_pact.pact_markers import HONOURS_PACT_WITH - - -class ProviderExecutor(Executor): - REQUIRED_PARAMETERS = [STATE, PACT_URI, HONOURS_PACT_WITH] - - def set_up(self): - pass - - def tear_down(self): - pass diff --git a/pytest_pact/standard_executor.py b/pytest_pact/standard_executor.py deleted file mode 100644 index 5e8d1c6..0000000 --- a/pytest_pact/standard_executor.py +++ /dev/null @@ -1,5 +0,0 @@ -from pytest_pact.executor import Executor - - -class StandardExecutor(Executor): - pass diff --git a/pytest_pact/utils/__init__.py b/pytest_pact/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pytest_pact/pact_utils.py b/pytest_pact/utils/pact_utils.py similarity index 77% rename from pytest_pact/pact_utils.py rename to pytest_pact/utils/pact_utils.py index e6aaa18..6f558b6 100644 --- a/pytest_pact/pact_utils.py +++ b/pytest_pact/utils/pact_utils.py @@ -1,12 +1,12 @@ import pytest -from pytest_pact.pytest_utils import read_marker +from pytest_pact.utils.pytest_utils import read_marker from pytest_pact.pact_markers import CONSUMER from pytest_pact.pact_markers import PROVIDER from pytest_pact.pact_markers import HAS_PACT_WITH from pytest_pact.pact_markers import HONOURS_PACT_WITH -from pytest_pact.standard_executor import StandardExecutor -from pytest_pact.consumer_executor import ConsumerExecutor -from pytest_pact.provider_executor import ProviderExecutor +from pytest_pact.executors.standard_executor import StandardExecutor +from pytest_pact.executors.consumer_executor import ConsumerExecutor +from pytest_pact.executors.provider_executor import ProviderExecutor def executor(pyfuncitem): diff --git a/pytest_pact/pytest_utils.py b/pytest_pact/utils/pytest_utils.py similarity index 100% rename from pytest_pact/pytest_utils.py rename to pytest_pact/utils/pytest_utils.py diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 18fc441..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest>=3.0 -pytest-runner diff --git a/setup.cfg b/setup.cfg index 58815e6..42ee8da 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,10 @@ +[bdist_wheel] +universal=1 + [aliases] test=pytest [tool:pytest] addopts = --pep8 +testpaths=tests +python_files=*.py diff --git a/setup.py b/setup.py index f1e2b1a..a2b60b5 100644 --- a/setup.py +++ b/setup.py @@ -7,16 +7,11 @@ author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), - license='LICENSE.txt', + license='LICENSE', long_description=open('README.rst').read(), description='Python implementation for Pact (http://pact.io/)', install_requires=[], setup_requires=['pytest-runner'], - tests_require=['pytest', 'pytest-pep8', 'pytest-sugar'], - url='https://github.com/Kalimaha/pytest-pact/', - entry_points={ - 'pytest11': [ - 'pytest-pact = pact', - ] - } + tests_require=['pytest>=3.0', 'pytest-pep8', 'pytest-sugar', 'pytest-mock'], + url='https://github.com/Kalimaha/pytest-pact/' ) diff --git a/tests/executors/__init__.py b/tests/executors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_consumer_executor.py b/tests/executors/test_consumer_executor.py similarity index 94% rename from tests/test_consumer_executor.py rename to tests/executors/test_consumer_executor.py index a906600..d584603 100644 --- a/tests/test_consumer_executor.py +++ b/tests/executors/test_consumer_executor.py @@ -1,4 +1,4 @@ -from pytest_pact.consumer_executor import ConsumerExecutor +from pytest_pact.executors.consumer_executor import ConsumerExecutor def test_valid_setup(): diff --git a/tests/executors/test_provider_executor.py b/tests/executors/test_provider_executor.py new file mode 100644 index 0000000..8f7483d --- /dev/null +++ b/tests/executors/test_provider_executor.py @@ -0,0 +1,129 @@ +import os +import imp +import pytest +from pytest_pact.executors.provider_executor import ProviderExecutor + + +def test_set_up(mocker): + class PactHelper(object): + def set_up(self): + pass + + pact_helper = PactHelper() + mocker.spy(pact_helper, 'set_up') + + executor = ProviderExecutor(FakePyFuncItem()) + mocker.patch.object(executor, 'set_up', new=pact_helper.set_up) + + executor.set_up() + assert pact_helper.set_up.call_count == 1 + + +def test_tear_down(mocker): + class PactHelper(object): + def tear_down(self): + pass + + pact_helper = PactHelper() + mocker.spy(pact_helper, 'tear_down') + + executor = ProviderExecutor(FakePyFuncItem()) + mocker.patch.object(executor, 'tear_down', new=pact_helper.tear_down) + + executor.set_up() + executor.tear_down() + assert pact_helper.tear_down.call_count == 1 + + +def test_load_pact_helper(monkeypatch): + executor = ProviderExecutor(FakePyFuncItem()) + + helper_path = os.getcwd() + '/tests/resources/pact_helper.py' + pact_helper = executor.load_pact_helper(helper_path) + + assert pact_helper is not None + + +def test_pact_helper_has_no_setup_method(): + executor = ProviderExecutor(FakePyFuncItem()) + + helper_path = os.getcwd() + '/tests/resources/pact_helper_no_setup.py' + + err_msg = 'Module pact_helper MUST have a set_up method.' + try: + executor.load_pact_helper(helper_path) + except Exception as e: + assert str(e) == err_msg + + +def test_pact_helper_has_no_teardown_method(): + executor = ProviderExecutor(FakePyFuncItem()) + + helper_path = os.getcwd() + '/tests/resources/pact_helper_no_teardown.py' + + err_msg = 'Module pact_helper MUST have a tear_down method.' + try: + executor.load_pact_helper(helper_path) + except Exception as e: + assert str(e) == err_msg + + +def test_pact_helper_path(monkeypatch): + executor = ProviderExecutor(FakePyFuncItem()) + + def list_files(dir): + return ['spam.py', 'eggs.py', 'pact_helper.py'] + monkeypatch.setattr(os, 'listdir', list_files) + + helper_path = os.getcwd() + '/tests/resources/pact_helper.py' + assert executor.pact_helper_path() == helper_path + + +def test_pact_helper_not_found(monkeypatch): + executor = ProviderExecutor(FakePyFuncItem()) + + def list_files(dir): + return ['spam.py', 'eggs.py', 'bacon.py'] + monkeypatch.setattr(os, 'listdir', list_files) + + err_msg = 'Could\'n find "pact_helper.py" script in Pact test directory.' + try: + executor.pact_helper_path() + except Exception as e: + assert str(e) == err_msg + + +def test_valid_setup(): + assert ProviderExecutor(FakePyFuncItem()).is_valid() + + +def test_missing_markers(mocker): + def get_marker(name): + return FakeMarker(name, name) if name is 'state' else None + + pyfuncitem = FakePyFuncItem() + mocker.patch.object(pyfuncitem, 'get_marker', new=get_marker) + + assert ProviderExecutor(pyfuncitem).is_valid() is False + + +def test_missing_values(mocker): + def get_marker(name): + return FakeMarker(name, None) if name is 'state' else None + + pyfuncitem = FakePyFuncItem() + mocker.patch.object(pyfuncitem, 'get_marker', new=get_marker) + + assert ProviderExecutor(pyfuncitem).is_valid() is False + + +class FakeMarker(object): + def __init__(self, marker_name, marker_value): + self.args = [marker_name + '_VALUE'] + + +class FakePyFuncItem(object): + fspath = os.getcwd() + '/tests/resources/my_test.py' + + def get_marker(self, marker_name): + return FakeMarker(marker_name, marker_name) diff --git a/tests/resources/__init__.py b/tests/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/resources/pact_helper.py b/tests/resources/pact_helper.py new file mode 100644 index 0000000..6000793 --- /dev/null +++ b/tests/resources/pact_helper.py @@ -0,0 +1,6 @@ +def set_up(): + print('Starting provider...') + + +def tear_down(): + print('Shutting down provider...') diff --git a/tests/resources/pact_helper_no_setup.py b/tests/resources/pact_helper_no_setup.py new file mode 100644 index 0000000..0ff7fe3 --- /dev/null +++ b/tests/resources/pact_helper_no_setup.py @@ -0,0 +1,2 @@ +def tear_down(): + print('Shutting down provider...') diff --git a/tests/resources/pact_helper_no_teardown.py b/tests/resources/pact_helper_no_teardown.py new file mode 100644 index 0000000..2053adc --- /dev/null +++ b/tests/resources/pact_helper_no_teardown.py @@ -0,0 +1,2 @@ +def set_up(): + print('Starting provider...') diff --git a/tests/test_books_service.py b/tests/test_books_service.py deleted file mode 100644 index 3784dc8..0000000 --- a/tests/test_books_service.py +++ /dev/null @@ -1,23 +0,0 @@ -from pact import * - - -@base_uri('localhost:1234') -@service_consumer('Library App') -@has_pact_with('Books Service') -class TestBooksService(): - - expected_response = { - 'status': 200, - 'headers': {'Content-Type': 'application/json'}, - 'body': { - 'id': '123', - 'title': 'A Fortune-Teller Told Me' - } - } - - @given('some books exist') - @upon_receiving('a request for a book') - @with_request({'method': 'get', 'path': '/books/123'}) - @will_respond_with(expected_response) - def test_get_book(self): - pass diff --git a/tests/test_pact_utils.py b/tests/test_pact_utils.py deleted file mode 100644 index acf5232..0000000 --- a/tests/test_pact_utils.py +++ /dev/null @@ -1,73 +0,0 @@ -from pytest_pact.pact_utils import * - - -def test_standard_executor(): - - class FakePyFuncItem(object): - def get_marker(self, marker_name): - return None - - pyfuncitem = FakePyFuncItem() - assert type(executor(pyfuncitem)).__name__ is 'StandardExecutor' - - -def test_consumer_executor(): - - class FakeMarker(object): - args = ['Books Service'] - - class FakePyFuncItem(object): - def get_marker(self, marker_name): - return FakeMarker() if marker_name == HAS_PACT_WITH else None - - pyfuncitem = FakePyFuncItem() - assert type(executor(pyfuncitem)).__name__ is 'ConsumerExecutor' - - -def test_provider_executor(): - - class FakeMarker(object): - args = ['Library App'] - - class FakePyFuncItem(object): - def get_marker(self, marker_name): - return FakeMarker() if marker_name == HONOURS_PACT_WITH else None - - pyfuncitem = FakePyFuncItem() - assert type(executor(pyfuncitem)).__name__ is 'ProviderExecutor' - - -def test_is_standard_test(): - - class FakePyFuncItem(object): - def get_marker(self, marker_name): - return None - - pyfuncitem = FakePyFuncItem() - assert pact_type(pyfuncitem) is None - - -def test_is_consumer_test(): - - class FakeMarker(object): - args = ['Books Service'] - - class FakePyFuncItem(object): - def get_marker(self, marker_name): - return FakeMarker() if marker_name == HAS_PACT_WITH else None - - pyfuncitem = FakePyFuncItem() - assert pact_type(pyfuncitem) == CONSUMER - - -def test_is_provider_test(): - - class FakeMarker(object): - args = ['Library App'] - - class FakePyFuncItem(object): - def get_marker(self, marker_name): - return FakeMarker() if marker_name == HONOURS_PACT_WITH else None - - pyfuncitem = FakePyFuncItem() - assert pact_type(pyfuncitem) == PROVIDER diff --git a/tests/test_provider_executor.py b/tests/test_provider_executor.py deleted file mode 100644 index 9f7c403..0000000 --- a/tests/test_provider_executor.py +++ /dev/null @@ -1,37 +0,0 @@ -from pytest_pact.provider_executor import ProviderExecutor - - -def test_valid_setup(): - class FakeMarker(object): - def __init__(self, marker_name): - self.args = [marker_name + '_VALUE'] - - class FakePyFuncItem(object): - def get_marker(self, marker_name): - return FakeMarker(marker_name) - pyfuncitem = FakePyFuncItem() - assert ProviderExecutor(pyfuncitem).is_valid() - - -def test_missing_markers(): - class FakeMarker(object): - def __init__(self, marker_name): - self.args = [marker_name + '_VALUE'] - - class FakePyFuncItem(object): - def get_marker(self, marker_name): - return FakeMarker(marker_name) if marker_name is 'state' else None - pyfuncitem = FakePyFuncItem() - assert ProviderExecutor(pyfuncitem).is_valid() is False - - -def test_null_values(): - class FakeMarker(object): - def __init__(self, marker_name): - self.args = [42] if marker_name is 'state' else [None] - - class FakePyFuncItem(object): - def get_marker(self, marker_name): - return FakeMarker(marker_name) - pyfuncitem = FakePyFuncItem() - assert ProviderExecutor(pyfuncitem).is_valid() is False diff --git a/tests/test_pypact.py b/tests/test_pypact.py deleted file mode 100644 index e03f19a..0000000 --- a/tests/test_pypact.py +++ /dev/null @@ -1 +0,0 @@ -from pact import * diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/utils/test_pact_utils.py b/tests/utils/test_pact_utils.py new file mode 100644 index 0000000..dcb34bb --- /dev/null +++ b/tests/utils/test_pact_utils.py @@ -0,0 +1,82 @@ +from pytest_pact.utils.pact_utils import * + + +def test_standard_executor(mocker): + pyfuncitem = FakePyFuncItem() + mocker.patch.object(pyfuncitem, 'get_marker') + pyfuncitem.get_marker.return_value = None + + assert type(executor(pyfuncitem)).__name__ is 'StandardExecutor' + + +def test_consumer_executor(mocker): + def get_marker(marker_name): + return pymarker if marker_name == HAS_PACT_WITH else None + + pyfuncitem = FakePyFuncItem() + mocker.patch.object(pyfuncitem, 'get_marker', new=get_marker) + + pymarker = FakeMarker() + mocker.patch.object(pymarker, 'args') + pymarker.args.return_value = ['Books Service'] + + assert type(executor(pyfuncitem)).__name__ is 'ConsumerExecutor' + + +def test_provider_executor(mocker): + def get_marker(marker_name): + return pymarker if marker_name == HONOURS_PACT_WITH else None + + pyfuncitem = FakePyFuncItem() + mocker.patch.object(pyfuncitem, 'get_marker', new=get_marker) + + pymarker = FakeMarker() + mocker.patch.object(pymarker, 'args') + pymarker.args.return_value = ['Hallo world!'] + + assert type(executor(pyfuncitem)).__name__ is 'ProviderExecutor' + + +def test_is_standard_test(mocker): + pyfuncitem = FakePyFuncItem() + mocker.patch.object(pyfuncitem, 'get_marker') + pyfuncitem.get_marker.return_value = None + + assert pact_type(pyfuncitem) is None + + +def test_is_consumer_test(mocker): + def get_marker(marker_name): + return pymarker if marker_name == HAS_PACT_WITH else None + + pyfuncitem = FakePyFuncItem() + mocker.patch.object(pyfuncitem, 'get_marker', new=get_marker) + + pymarker = FakeMarker() + mocker.patch.object(pymarker, 'args') + pymarker.args.return_value = ['Hallo world!'] + + assert pact_type(pyfuncitem) == CONSUMER + + +def test_is_provider_test(mocker): + def get_marker(marker_name): + return pymarker if marker_name == HONOURS_PACT_WITH else None + + pyfuncitem = FakePyFuncItem() + mocker.patch.object(pyfuncitem, 'get_marker', new=get_marker) + + pymarker = FakeMarker() + mocker.patch.object(pymarker, 'args') + pymarker.args.return_value = ['Hallo world!'] + + assert pact_type(pyfuncitem) == PROVIDER + + +class FakePyFuncItem(object): + def get_marker(self, marker_name): + return None + + +class FakeMarker(object): + args = [] diff --git a/tests/test_pytest_utils.py b/tests/utils/test_pytest_utils.py similarity index 57% rename from tests/test_pytest_utils.py rename to tests/utils/test_pytest_utils.py index c2f47d8..b86aac2 100644 --- a/tests/test_pytest_utils.py +++ b/tests/utils/test_pytest_utils.py @@ -1,19 +1,16 @@ -from pytest_pact.pytest_utils import * +from pytest_pact.utils.pytest_utils import * def test_read_existing_marker(): - expected_marker = 'some books exist' - class FakeMarker(object): - args = [expected_marker] + args = ['My Value'] class FakePyFuncItem(object): def get_marker(self, marker_name): return FakeMarker() pyfuncitem = FakePyFuncItem() - marker_name = 'given' - assert read_marker(pyfuncitem, marker_name) == expected_marker + assert read_marker(pyfuncitem, 'My Marker') == 'My Value' def test_read_non_existing_marker(): @@ -23,5 +20,4 @@ def get_marker(self, marker_name): return None pyfuncitem = FakePyFuncItem() - marker_name = 'given' - assert read_marker(pyfuncitem, marker_name) is None + assert read_marker(pyfuncitem, 'My Marker') is None From de6d16a541789f3e785d78d80510721ef46acf5a Mon Sep 17 00:00:00 2001 From: Guido Barbaglia Date: Wed, 17 May 2017 17:24:27 +1000 Subject: [PATCH 06/85] Executor for Provider tests. --- pytest_pact/executors/provider_executor.py | 64 +++++++- setup.py | 7 +- tests/executors/test_provider_executor.py | 171 ++++++++++++++++++++- tests/resources/simple_pact.json | 38 +++++ 4 files changed, 276 insertions(+), 4 deletions(-) create mode 100644 tests/resources/simple_pact.json diff --git a/pytest_pact/executors/provider_executor.py b/pytest_pact/executors/provider_executor.py index f30d153..b851a7b 100644 --- a/pytest_pact/executors/provider_executor.py +++ b/pytest_pact/executors/provider_executor.py @@ -1,6 +1,11 @@ import os import sys import imp +import json +import urllib.parse +import urllib.request +from urllib.request import Request +from pytest_pact.utils.pytest_utils import read_marker from pytest_pact.executors.executor import Executor from pytest_pact.pact_markers import STATE from pytest_pact.pact_markers import PACT_URI @@ -13,13 +18,70 @@ class ProviderExecutor(Executor): PACT_HELPER_NOT_FOUND = 'Could\'n find "pact_helper.py" script in Pact test directory.' SET_UP_NOT_FOUND = 'Module pact_helper MUST have a set_up method' TEAR_DOWN_NOT_FOUND = 'Module pact_helper MUST have a tear_down method' - pact_helper = None + MISSING_STATE_SETUP = 'Missing state setup' + BASE_URL = 'http://localhost:1234' def set_up(self): path_to_pact_helper = self.pact_helper_path() self.pact_helper = self.load_pact_helper(path_to_pact_helper) self.pact_helper.set_up() + def verify_pact(self): + interactions = self.load_interactions() + state = read_marker(self.pyfuncitem, STATE) + for interaction in interactions: + if interaction['providerState'] == state: + return self.verify_interaction(interaction) + + def verify_interaction(self, interaction): + req = self.build_request(interaction['request']) + response = urllib.request.urlopen(req) + consumer_response = interaction['response'] + + response_headers = response.getheaders() + response_status = response.status + response_reason = response.reason + response_body = json.loads(response.read().decode()) + + status_matches = self.status_matches(consumer_response['status'], response_status) + headers_match = self.headers_match(consumer_response['headers'], response_headers) + body_matches = self.body_matches(consumer_response['body'], response_body) + + return status_matches and headers_match and body_matches + + def body_matches(self, consumer_body, response_body): + return consumer_body.items() <= response_body.items() + + def headers_match(self, consumer_headers, response_headers): + return consumer_headers.items() <= dict(response_headers).items() + + def reason_matches(self, consumer_reason, response_reason): + return consumer_reason == response_reason + + def status_matches(self, consumer_status, response_status): + return consumer_status == response_status + + def build_request(self, consumer_request): + url = urllib.parse.urljoin(self.BASE_URL, consumer_request['path']) + url += consumer_request.get('query', '') + method = consumer_request.get('method', 'GET') + headers = consumer_request.get('headers', {}) + data = consumer_request.get('body', {}) + return Request(url=url, method=method, headers=headers, data=data) + + def load_interactions(self): + return self.fetch_pact_file()['interactions'] + + def fetch_pact_file(self): + pact_uri = read_marker(self.pyfuncitem, PACT_URI) + if pact_uri.startswith(('http://', 'https://')): + with urllib.request.urlopen(pact_uri) as f: + pact = json.load(f) + else: + with open(pact_uri) as f: + pact = json.load(f) + return pact + def tear_down(self): self.pact_helper.tear_down() diff --git a/setup.py b/setup.py index a2b60b5..0ff222f 100644 --- a/setup.py +++ b/setup.py @@ -13,5 +13,10 @@ install_requires=[], setup_requires=['pytest-runner'], tests_require=['pytest>=3.0', 'pytest-pep8', 'pytest-sugar', 'pytest-mock'], - url='https://github.com/Kalimaha/pytest-pact/' + url='https://github.com/Kalimaha/pytest-pact/', + entry_points = { + 'pytest11': [ + 'pytest-pact = pytest_pact.pact', + ] + } ) diff --git a/tests/executors/test_provider_executor.py b/tests/executors/test_provider_executor.py index 8f7483d..873e8b8 100644 --- a/tests/executors/test_provider_executor.py +++ b/tests/executors/test_provider_executor.py @@ -1,9 +1,169 @@ import os import imp +import json import pytest +import urllib.request +from urllib.request import Request from pytest_pact.executors.provider_executor import ProviderExecutor +def test_verify_pact(mocker): + class Marker(object): + args = ['a menu exists'] + + class Item(object): + def get_marker(self, name): + return Marker() + + pact_file = simple_pact() + executor = ProviderExecutor(Item()) + mocker.patch.object(executor, 'fetch_pact_file') + mocker.patch.object(executor, 'verify_interaction') + executor.fetch_pact_file.return_value = pact_file + executor.verify_interaction.return_value = True + + assert executor.verify_pact() is True + + +def test_verify_interaction(monkeypatch): + class FakeReader(object): + def decode(self): + return str({ + "id": 42, + "name": "Spam, Eggs and Bacon", + "ingredients": ["spam", "eggs", "bacon"] + }).replace("'", "\"") + + class FakeResponse(object): + status = 200 + reason = 'OK' + + def getheaders(self): + return [ + ("Content-Type", "application/json"), + ("Pragma", "no-cache") + ] + + def read(self): + return FakeReader() + + executor = ProviderExecutor(None) + interaction = simple_pact()['interactions'][0] + + def urlopen(url): + return FakeResponse() + monkeypatch.setattr(urllib.request, 'urlopen', urlopen) + + assert executor.verify_interaction(interaction) is True + + +def test_body_matches(): + executor = ProviderExecutor(None) + consumer_body = {"name": "Spam, Eggs and Bacon"} + response_body = { + "name": "Spam, Eggs and Bacon", + "ingredients": ["spam", "eggs", "bacon"], + "vegan": False, + "vegetarian": False + } + + assert executor.body_matches(consumer_body, response_body) is True + + +def test_headers_match(): + executor = ProviderExecutor(None) + consumer_headers = { + "Content-Type": "application/json", + "Pragma": "no-cache" + } + response_headers = [ + ('Content-Type', 'application/json'), + ('Content-Length', '292'), + ('Access-Control-Allow-Credentials', 'true'), + ('Pragma', 'no-cache') + ] + + assert executor.headers_match(consumer_headers, response_headers) is True + + +def test_reason_matches(): + executor = ProviderExecutor(None) + consumer_reason = "I'm a teapot." + response_reason = "I'm a teapot." + + assert executor.reason_matches(consumer_reason, response_reason) is True + + +def test_status_matches(): + executor = ProviderExecutor(None) + consumer_status = 418 + response_status = 418 + + assert executor.status_matches(consumer_status, response_status) is True + + +def test_build_request(): + executor = ProviderExecutor(None) + consumer_request = { + "method": "POST", + "path": "/menu/42", + "query": "?vegan=false", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "alligator": { + "name": "Mary" + } + } + } + request = executor.build_request(consumer_request) + assert request.get_method() == 'POST' + assert request.full_url == 'http://localhost:1234/menu/42?vegan=false' + assert request.headers == {"Content-type": "application/json"} + assert request.data == {"alligator": {"name": "Mary"}} + + +def test_load_interactions(): + class Marker(object): + args = [os.getcwd() + '/tests/resources/simple_pact.json'] + + class Item(object): + def get_marker(self, name): + return Marker() + + executor = ProviderExecutor(Item()) + expected_interactions = simple_pact()['interactions'] + + assert executor.load_interactions() == expected_interactions + + +def test_fetch_remote_pact_file(monkeypatch): + class Marker(object): + args = ['http://test.com/simple_pact.json'] + + class Item(object): + def get_marker(self, name): + return Marker() + + def url_open(_): + return open(os.getcwd() + '/tests/resources/simple_pact.json') + monkeypatch.setattr(urllib.request, 'urlopen', url_open) + + assert ProviderExecutor(Item()).fetch_pact_file() == simple_pact() + + +def test_fetch_local_pact_file(): + class Marker(object): + args = [os.getcwd() + '/tests/resources/simple_pact.json'] + + class Item(object): + def get_marker(self, name): + return Marker() + + assert ProviderExecutor(Item()).fetch_pact_file() == simple_pact() + + def test_set_up(mocker): class PactHelper(object): def set_up(self): @@ -12,7 +172,7 @@ def set_up(self): pact_helper = PactHelper() mocker.spy(pact_helper, 'set_up') - executor = ProviderExecutor(FakePyFuncItem()) + executor = ProviderExecutor(None) mocker.patch.object(executor, 'set_up', new=pact_helper.set_up) executor.set_up() @@ -35,7 +195,7 @@ def tear_down(self): assert pact_helper.tear_down.call_count == 1 -def test_load_pact_helper(monkeypatch): +def test_load_pact_helper(): executor = ProviderExecutor(FakePyFuncItem()) helper_path = os.getcwd() + '/tests/resources/pact_helper.py' @@ -127,3 +287,10 @@ class FakePyFuncItem(object): def get_marker(self, marker_name): return FakeMarker(marker_name, marker_name) + + +def simple_pact(): + path_to_pact = os.getcwd() + '/tests/resources/simple_pact.json' + with open(path_to_pact) as f: + pact = json.load(f) + return pact diff --git a/tests/resources/simple_pact.json b/tests/resources/simple_pact.json new file mode 100644 index 0000000..d3f236a --- /dev/null +++ b/tests/resources/simple_pact.json @@ -0,0 +1,38 @@ +{ + "provider": { + "name": "Restaurant" + }, + "consumer": { + "name": "Consumer" + }, + "interactions": [ + { + "providerState" : "a menu exists", + "description": "a request for an item on the menu", + "request": { + "method": "GET", + "path": "/menu/42", + "query": "" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json", + "Pragma": "no-cache" + }, + "body": { + "name": "Spam, Eggs and Bacon", + "ingredients": ["spam", "eggs", "bacon"] + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "1.0.0" + }, + "pact-jvm": { + "version": "1.0.0" + } + } +} From b720362c3c8f8d0a66b6e104f211f27ad1c5cb67 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 20 May 2017 16:11:55 +1000 Subject: [PATCH 07/85] Rename library. --- .gitignore | 8 +++++--- flask_service.py | 5 ----- {pytest_pact => pact_test}/__init__.py | 0 .../constants/__init__.py | 0 .../constants/constants.py | 0 .../executors/__init__.py | 0 pact_test/executors/consumer_executor.py | 20 +++++++++++++++++++ .../executors/executor.py | 2 +- .../executors/provider_executor.py | 10 +++++----- pact_test/executors/standard_executor.py | 5 +++++ {pytest_pact => pact_test}/pact_markers.py | 0 {pytest_pact => pact_test}/utils/__init__.py | 0 .../utils/pact_utils.py | 16 +++++++-------- .../utils/pytest_utils.py | 0 pytest_pact/executors/consumer_executor.py | 20 ------------------- pytest_pact/executors/standard_executor.py | 5 ----- setup.py | 9 ++------- tests/executors/test_consumer_executor.py | 2 +- tests/executors/test_provider_executor.py | 2 +- tests/utils/test_pact_utils.py | 2 +- tests/utils/test_pytest_utils.py | 2 +- 21 files changed, 50 insertions(+), 58 deletions(-) delete mode 100644 flask_service.py rename {pytest_pact => pact_test}/__init__.py (100%) rename {pytest_pact => pact_test}/constants/__init__.py (100%) rename {pytest_pact => pact_test}/constants/constants.py (100%) rename {pytest_pact => pact_test}/executors/__init__.py (100%) create mode 100644 pact_test/executors/consumer_executor.py rename {pytest_pact => pact_test}/executors/executor.py (92%) rename {pytest_pact => pact_test}/executors/provider_executor.py (93%) create mode 100644 pact_test/executors/standard_executor.py rename {pytest_pact => pact_test}/pact_markers.py (100%) rename {pytest_pact => pact_test}/utils/__init__.py (100%) rename {pytest_pact => pact_test}/utils/pact_utils.py (60%) rename {pytest_pact => pact_test}/utils/pytest_utils.py (100%) delete mode 100644 pytest_pact/executors/consumer_executor.py delete mode 100644 pytest_pact/executors/standard_executor.py diff --git a/.gitignore b/.gitignore index baf665e..c00e2dd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,14 +2,16 @@ .cache/ .idea/ build/ -pypact.egg-info/ +pact_test.egg-info/ tests/__pycache__/ .eggs/ .coverage *.log *.egg *.iml -pytest_pact.egg-info/ -pytest_pact/__pycache__ +pact_test.egg-info/ +pact_test/__pycache__ __pycache__ .DS_Store +pytest_pact.egg-info/ +dist/ diff --git a/flask_service.py b/flask_service.py deleted file mode 100644 index f5574e5..0000000 --- a/flask_service.py +++ /dev/null @@ -1,5 +0,0 @@ -from flask import Flask -from flask import request - - -app = Flask(__name__) diff --git a/pytest_pact/__init__.py b/pact_test/__init__.py similarity index 100% rename from pytest_pact/__init__.py rename to pact_test/__init__.py diff --git a/pytest_pact/constants/__init__.py b/pact_test/constants/__init__.py similarity index 100% rename from pytest_pact/constants/__init__.py rename to pact_test/constants/__init__.py diff --git a/pytest_pact/constants/constants.py b/pact_test/constants/constants.py similarity index 100% rename from pytest_pact/constants/constants.py rename to pact_test/constants/constants.py diff --git a/pytest_pact/executors/__init__.py b/pact_test/executors/__init__.py similarity index 100% rename from pytest_pact/executors/__init__.py rename to pact_test/executors/__init__.py diff --git a/pact_test/executors/consumer_executor.py b/pact_test/executors/consumer_executor.py new file mode 100644 index 0000000..f128b94 --- /dev/null +++ b/pact_test/executors/consumer_executor.py @@ -0,0 +1,20 @@ +from pact_test.executors.executor import Executor +from pact_test.pact_markers import SERVICE_CONSUMER +from pact_test.pact_markers import HAS_PACT_WITH +from pact_test.pact_markers import BASE_URI +from pact_test.pact_markers import GIVEN +from pact_test.pact_markers import UPON_RECEIVING +from pact_test.pact_markers import WITH_REQUEST +from pact_test.pact_markers import WILL_RESPOND_WITH + + +class ConsumerExecutor(Executor): + REQUIRED_PARAMETERS = [SERVICE_CONSUMER, HAS_PACT_WITH, BASE_URI, + GIVEN, UPON_RECEIVING, WITH_REQUEST, + WILL_RESPOND_WITH] + + def set_up(self): + pass + + def tear_down(self): + pass diff --git a/pytest_pact/executors/executor.py b/pact_test/executors/executor.py similarity index 92% rename from pytest_pact/executors/executor.py rename to pact_test/executors/executor.py index 88fbc69..3895fe5 100644 --- a/pytest_pact/executors/executor.py +++ b/pact_test/executors/executor.py @@ -1,4 +1,4 @@ -from pytest_pact.utils.pytest_utils import read_marker +from pact_test.utils.pytest_utils import read_marker class Executor(object): diff --git a/pytest_pact/executors/provider_executor.py b/pact_test/executors/provider_executor.py similarity index 93% rename from pytest_pact/executors/provider_executor.py rename to pact_test/executors/provider_executor.py index b851a7b..3e1fb8f 100644 --- a/pytest_pact/executors/provider_executor.py +++ b/pact_test/executors/provider_executor.py @@ -5,11 +5,11 @@ import urllib.parse import urllib.request from urllib.request import Request -from pytest_pact.utils.pytest_utils import read_marker -from pytest_pact.executors.executor import Executor -from pytest_pact.pact_markers import STATE -from pytest_pact.pact_markers import PACT_URI -from pytest_pact.pact_markers import HONOURS_PACT_WITH +from pact_test.utils.pytest_utils import read_marker +from pact_test.executors.executor import Executor +from pact_test.pact_markers import STATE +from pact_test.pact_markers import PACT_URI +from pact_test.pact_markers import HONOURS_PACT_WITH class ProviderExecutor(Executor): diff --git a/pact_test/executors/standard_executor.py b/pact_test/executors/standard_executor.py new file mode 100644 index 0000000..10f6fbe --- /dev/null +++ b/pact_test/executors/standard_executor.py @@ -0,0 +1,5 @@ +from pact_test.executors.executor import Executor + + +class StandardExecutor(Executor): + pass diff --git a/pytest_pact/pact_markers.py b/pact_test/pact_markers.py similarity index 100% rename from pytest_pact/pact_markers.py rename to pact_test/pact_markers.py diff --git a/pytest_pact/utils/__init__.py b/pact_test/utils/__init__.py similarity index 100% rename from pytest_pact/utils/__init__.py rename to pact_test/utils/__init__.py diff --git a/pytest_pact/utils/pact_utils.py b/pact_test/utils/pact_utils.py similarity index 60% rename from pytest_pact/utils/pact_utils.py rename to pact_test/utils/pact_utils.py index 6f558b6..a216c1d 100644 --- a/pytest_pact/utils/pact_utils.py +++ b/pact_test/utils/pact_utils.py @@ -1,12 +1,12 @@ import pytest -from pytest_pact.utils.pytest_utils import read_marker -from pytest_pact.pact_markers import CONSUMER -from pytest_pact.pact_markers import PROVIDER -from pytest_pact.pact_markers import HAS_PACT_WITH -from pytest_pact.pact_markers import HONOURS_PACT_WITH -from pytest_pact.executors.standard_executor import StandardExecutor -from pytest_pact.executors.consumer_executor import ConsumerExecutor -from pytest_pact.executors.provider_executor import ProviderExecutor +from pact_test.utils.pytest_utils import read_marker +from pact_test.pact_markers import CONSUMER +from pact_test.pact_markers import PROVIDER +from pact_test.pact_markers import HAS_PACT_WITH +from pact_test.pact_markers import HONOURS_PACT_WITH +from pact_test.executors.standard_executor import StandardExecutor +from pact_test.executors.consumer_executor import ConsumerExecutor +from pact_test.executors.provider_executor import ProviderExecutor def executor(pyfuncitem): diff --git a/pytest_pact/utils/pytest_utils.py b/pact_test/utils/pytest_utils.py similarity index 100% rename from pytest_pact/utils/pytest_utils.py rename to pact_test/utils/pytest_utils.py diff --git a/pytest_pact/executors/consumer_executor.py b/pytest_pact/executors/consumer_executor.py deleted file mode 100644 index 1537a95..0000000 --- a/pytest_pact/executors/consumer_executor.py +++ /dev/null @@ -1,20 +0,0 @@ -from pytest_pact.executors.executor import Executor -from pytest_pact.pact_markers import SERVICE_CONSUMER -from pytest_pact.pact_markers import HAS_PACT_WITH -from pytest_pact.pact_markers import BASE_URI -from pytest_pact.pact_markers import GIVEN -from pytest_pact.pact_markers import UPON_RECEIVING -from pytest_pact.pact_markers import WITH_REQUEST -from pytest_pact.pact_markers import WILL_RESPOND_WITH - - -class ConsumerExecutor(Executor): - REQUIRED_PARAMETERS = [SERVICE_CONSUMER, HAS_PACT_WITH, BASE_URI, - GIVEN, UPON_RECEIVING, WITH_REQUEST, - WILL_RESPOND_WITH] - - def set_up(self): - pass - - def tear_down(self): - pass diff --git a/pytest_pact/executors/standard_executor.py b/pytest_pact/executors/standard_executor.py deleted file mode 100644 index b4eddaa..0000000 --- a/pytest_pact/executors/standard_executor.py +++ /dev/null @@ -1,5 +0,0 @@ -from pytest_pact.executors.executor import Executor - - -class StandardExecutor(Executor): - pass diff --git a/setup.py b/setup.py index 0ff222f..bccb57a 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import find_packages setup( - name='pytest-pact', + name='pact-test', version='0.1.0', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', @@ -13,10 +13,5 @@ install_requires=[], setup_requires=['pytest-runner'], tests_require=['pytest>=3.0', 'pytest-pep8', 'pytest-sugar', 'pytest-mock'], - url='https://github.com/Kalimaha/pytest-pact/', - entry_points = { - 'pytest11': [ - 'pytest-pact = pytest_pact.pact', - ] - } + url='https://github.com/Kalimaha/pact-test/' ) diff --git a/tests/executors/test_consumer_executor.py b/tests/executors/test_consumer_executor.py index d584603..aba0988 100644 --- a/tests/executors/test_consumer_executor.py +++ b/tests/executors/test_consumer_executor.py @@ -1,4 +1,4 @@ -from pytest_pact.executors.consumer_executor import ConsumerExecutor +from pact_test.executors.consumer_executor import ConsumerExecutor def test_valid_setup(): diff --git a/tests/executors/test_provider_executor.py b/tests/executors/test_provider_executor.py index 873e8b8..fcb1e2d 100644 --- a/tests/executors/test_provider_executor.py +++ b/tests/executors/test_provider_executor.py @@ -4,7 +4,7 @@ import pytest import urllib.request from urllib.request import Request -from pytest_pact.executors.provider_executor import ProviderExecutor +from pact_test.executors.provider_executor import ProviderExecutor def test_verify_pact(mocker): diff --git a/tests/utils/test_pact_utils.py b/tests/utils/test_pact_utils.py index dcb34bb..c1e28ac 100644 --- a/tests/utils/test_pact_utils.py +++ b/tests/utils/test_pact_utils.py @@ -1,4 +1,4 @@ -from pytest_pact.utils.pact_utils import * +from pact_test.utils.pact_utils import * def test_standard_executor(mocker): diff --git a/tests/utils/test_pytest_utils.py b/tests/utils/test_pytest_utils.py index b86aac2..75215a6 100644 --- a/tests/utils/test_pytest_utils.py +++ b/tests/utils/test_pytest_utils.py @@ -1,4 +1,4 @@ -from pytest_pact.utils.pytest_utils import * +from pact_test.utils.pytest_utils import * def test_read_existing_marker(): From b81d90c6cfe8095108d5f80fe02072a152f7efe2 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 20 May 2017 16:51:36 +1000 Subject: [PATCH 08/85] Tox configuration. --- .gitignore | 1 + tox.ini | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index c00e2dd..9b62e8b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ __pycache__ .DS_Store pytest_pact.egg-info/ dist/ +.tox diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..eda5dc8 --- /dev/null +++ b/tox.ini @@ -0,0 +1,11 @@ +[tox] +# envlist = py27, py33, py34, py35, py36 +envlist = py36 + +[testenv] +deps = + pytest + pytest-pep8 + pytest-sugar + pytest-mock +commands=py.test --pep8 From 87877b05d2cbc7432a6a7f8c01f8cdee977cb7e5 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 20 May 2017 16:57:05 +1000 Subject: [PATCH 09/85] Travis & Tox. --- .travis.yml | 21 ++++++++++++--------- README.rst | 12 ++++-------- tox.ini | 15 ++++++++++++++- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index 23647a4..a21679c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,17 @@ language: python + +sudo: false + python: - - 2.7 - - 3.3 - - 3.4 - 3.5 - 3.6 + install: - - "pip install pytest pytest-cov pytest-pep8 pytest-sugar pytest-mock codecov" - - "pip install -e ." -script: - - py.test --cov=./ -after_success: - - codecov + - pip install tox-travis + - pip install pytest-cov + - pip install coveralls + - pip install -e . + +script: tox + +after_success: coveralls diff --git a/README.rst b/README.rst index d63c53c..2f965f8 100644 --- a/README.rst +++ b/README.rst @@ -1,13 +1,7 @@ .. image:: https://travis-ci.org/Kalimaha/pytest-pact.svg?branch=master :target: https://travis-ci.org/Kalimaha/pytest-pact -.. image:: https://codecov.io/gh/Kalimaha/pytest-pact/branch/master/graph/badge.svg - :target: https://codecov.io/gh/Kalimaha/pytest-pact -.. image:: https://img.shields.io/badge/python-2.7-blue.svg - :target: https://travis-ci.org/Kalimaha/pytest-pact -.. image:: https://img.shields.io/badge/python-3.3-blue.svg - :target: https://travis-ci.org/Kalimaha/pytest-pact -.. image:: https://img.shields.io/badge/python-3.4-blue.svg - :target: https://travis-ci.org/Kalimaha/pytest-pact +.. image:: https://coveralls.io/repos/github/Kalimaha/pact-test/badge.svg?branch=development + :target: https://coveralls.io/github/Kalimaha/pact-test?branch=development .. image:: https://img.shields.io/badge/python-3.5-blue.svg :target: https://travis-ci.org/Kalimaha/pytest-pact .. image:: https://img.shields.io/badge/python-3.6-blue.svg @@ -20,4 +14,6 @@ Python implementation for Pact (http://pact.io/) Setup ----- +.. code:: bash + python setup.py install diff --git a/tox.ini b/tox.ini index eda5dc8..4c067f7 100644 --- a/tox.ini +++ b/tox.ini @@ -8,4 +8,17 @@ deps = pytest-pep8 pytest-sugar pytest-mock -commands=py.test --pep8 + pytest-cov + coveralls +passenv = + CI + TOXENV + TRAVIS + TRAVIS_BUILD_ID + TRAVIS_BRANCH + TRAVIS_JOB_NUMBER + TRAVIS_PULL_REQUEST + TRAVIS_JOB_ID + TRAVIS_REPO_SLUG + TRAVIS_COMMIT +commands = py.test --cov pact_test --cov-report term-missing From ed22a1e588f57d5fe2d7902063aeb9d5d69c83b4 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 20 May 2017 18:01:14 +1000 Subject: [PATCH 10/85] Command line script. --- bin/pact-test | 27 +++++++++++++++++++++++++++ setup.py | 3 ++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 bin/pact-test diff --git a/bin/pact-test b/bin/pact-test new file mode 100644 index 0000000..fbb0d17 --- /dev/null +++ b/bin/pact-test @@ -0,0 +1,27 @@ +#!/usr/bin/env python +import sys + + +def print_help(): + print(' ') + print('================================') + print('Welcome to Pact Test for Python!') + print('--------------------------------') + print(' Available Commands ') + print(' ') + print('verify..........Verify Pact test') + print('================================') + print(' ') + + +def verify(): + pass + + +if len(sys.argv) is 1: + print_help() +else: + if sys.argv[1] == 'verify': + verify() + else: + print_help() diff --git a/setup.py b/setup.py index bccb57a..fa72fe7 100644 --- a/setup.py +++ b/setup.py @@ -13,5 +13,6 @@ install_requires=[], setup_requires=['pytest-runner'], tests_require=['pytest>=3.0', 'pytest-pep8', 'pytest-sugar', 'pytest-mock'], - url='https://github.com/Kalimaha/pact-test/' + url='https://github.com/Kalimaha/pact-test/', + scripts=['bin/pact-test'] ) From 2b2fdacb1a52667da12555518121fd7beeb2c1a1 Mon Sep 17 00:00:00 2001 From: Guido Barbaglia Date: Wed, 24 May 2017 13:28:05 +1000 Subject: [PATCH 11/85] Version bump for Python Wheel. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fa72fe7..42546b6 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.1.0', + version='0.1.1', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), From 407eb7f43e1aa2d87a9e5b6719d84f5bbf72f274 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Thu, 25 May 2017 19:11:11 +1000 Subject: [PATCH 12/85] Initial models for consumers and providers test. --- dist/.keep | 0 pact_test/constants/__init__.py | 1 - pact_test/constants/constants.py | 10 - pact_test/executors/consumer_executor.py | 20 -- pact_test/executors/executor.py | 24 -- pact_test/executors/provider_executor.py | 99 ------- pact_test/executors/standard_executor.py | 5 - pact_test/{executors => models}/__init__.py | 0 pact_test/models/service_consumer_test.py | 39 +++ pact_test/models/service_provider_test.py | 67 +++++ pact_test/pact_markers.py | 26 -- pact_test/utils/__init__.py | 0 pact_test/utils/pact_utils.py | 37 --- pact_test/utils/pytest_utils.py | 6 - tests/executors/__init__.py | 0 tests/executors/test_consumer_executor.py | 37 --- tests/executors/test_provider_executor.py | 296 -------------------- tests/models/test_service_consumer_test.py | 57 ++++ tests/models/test_service_provider_test.py | 80 ++++++ tests/utils/__init__.py | 0 tests/utils/test_pact_utils.py | 82 ------ tests/utils/test_pytest_utils.py | 23 -- tox.ini | 4 +- 23 files changed, 245 insertions(+), 668 deletions(-) delete mode 100644 dist/.keep delete mode 100644 pact_test/constants/__init__.py delete mode 100644 pact_test/constants/constants.py delete mode 100644 pact_test/executors/consumer_executor.py delete mode 100644 pact_test/executors/executor.py delete mode 100644 pact_test/executors/provider_executor.py delete mode 100644 pact_test/executors/standard_executor.py rename pact_test/{executors => models}/__init__.py (100%) create mode 100644 pact_test/models/service_consumer_test.py create mode 100644 pact_test/models/service_provider_test.py delete mode 100644 pact_test/pact_markers.py delete mode 100644 pact_test/utils/__init__.py delete mode 100644 pact_test/utils/pact_utils.py delete mode 100644 pact_test/utils/pytest_utils.py delete mode 100644 tests/executors/__init__.py delete mode 100644 tests/executors/test_consumer_executor.py delete mode 100644 tests/executors/test_provider_executor.py create mode 100644 tests/models/test_service_consumer_test.py create mode 100644 tests/models/test_service_provider_test.py delete mode 100644 tests/utils/__init__.py delete mode 100644 tests/utils/test_pact_utils.py delete mode 100644 tests/utils/test_pytest_utils.py diff --git a/dist/.keep b/dist/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/pact_test/constants/__init__.py b/pact_test/constants/__init__.py deleted file mode 100644 index b40dbcb..0000000 --- a/pact_test/constants/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__all__ = ['constants'] diff --git a/pact_test/constants/constants.py b/pact_test/constants/constants.py deleted file mode 100644 index d218474..0000000 --- a/pact_test/constants/constants.py +++ /dev/null @@ -1,10 +0,0 @@ -state = pytest.mark.state -given = pytest.mark.given -base_uri = pytest.mark.base_uri -pact_uri = pytest.mark.pact_uri -with_request = pytest.mark.with_request -has_pact_with = pytest.mark.has_pact_with -upon_receiving = pytest.mark.upon_receiving -will_respond_with = pytest.mark.will_respond_with -service_consumer = pytest.mark.service_consumer -honours_pact_with = pytest.mark.honours_pact_with diff --git a/pact_test/executors/consumer_executor.py b/pact_test/executors/consumer_executor.py deleted file mode 100644 index f128b94..0000000 --- a/pact_test/executors/consumer_executor.py +++ /dev/null @@ -1,20 +0,0 @@ -from pact_test.executors.executor import Executor -from pact_test.pact_markers import SERVICE_CONSUMER -from pact_test.pact_markers import HAS_PACT_WITH -from pact_test.pact_markers import BASE_URI -from pact_test.pact_markers import GIVEN -from pact_test.pact_markers import UPON_RECEIVING -from pact_test.pact_markers import WITH_REQUEST -from pact_test.pact_markers import WILL_RESPOND_WITH - - -class ConsumerExecutor(Executor): - REQUIRED_PARAMETERS = [SERVICE_CONSUMER, HAS_PACT_WITH, BASE_URI, - GIVEN, UPON_RECEIVING, WITH_REQUEST, - WILL_RESPOND_WITH] - - def set_up(self): - pass - - def tear_down(self): - pass diff --git a/pact_test/executors/executor.py b/pact_test/executors/executor.py deleted file mode 100644 index 3895fe5..0000000 --- a/pact_test/executors/executor.py +++ /dev/null @@ -1,24 +0,0 @@ -from pact_test.utils.pytest_utils import read_marker - - -class Executor(object): - REQUIRED_PARAMETERS = [] - - def __init__(self, pyfuncitem): - self.pyfuncitem = pyfuncitem - - def set_up(self): - pass - - def tear_down(self): - pass - - def is_valid(self): - check = {} - for marker in self.REQUIRED_PARAMETERS: - marker_value = read_marker(self.pyfuncitem, marker) - if marker_value is not None: - check[marker] = marker_value - valid_keys = sorted(check.keys()) == sorted(self.REQUIRED_PARAMETERS) - valid_values = all(v is not None for v in check.values()) - return valid_keys and valid_values diff --git a/pact_test/executors/provider_executor.py b/pact_test/executors/provider_executor.py deleted file mode 100644 index 3e1fb8f..0000000 --- a/pact_test/executors/provider_executor.py +++ /dev/null @@ -1,99 +0,0 @@ -import os -import sys -import imp -import json -import urllib.parse -import urllib.request -from urllib.request import Request -from pact_test.utils.pytest_utils import read_marker -from pact_test.executors.executor import Executor -from pact_test.pact_markers import STATE -from pact_test.pact_markers import PACT_URI -from pact_test.pact_markers import HONOURS_PACT_WITH - - -class ProviderExecutor(Executor): - REQUIRED_PARAMETERS = [STATE, PACT_URI, HONOURS_PACT_WITH] - PACT_HELPER = 'pact_helper.py' - PACT_HELPER_NOT_FOUND = 'Could\'n find "pact_helper.py" script in Pact test directory.' - SET_UP_NOT_FOUND = 'Module pact_helper MUST have a set_up method' - TEAR_DOWN_NOT_FOUND = 'Module pact_helper MUST have a tear_down method' - MISSING_STATE_SETUP = 'Missing state setup' - BASE_URL = 'http://localhost:1234' - - def set_up(self): - path_to_pact_helper = self.pact_helper_path() - self.pact_helper = self.load_pact_helper(path_to_pact_helper) - self.pact_helper.set_up() - - def verify_pact(self): - interactions = self.load_interactions() - state = read_marker(self.pyfuncitem, STATE) - for interaction in interactions: - if interaction['providerState'] == state: - return self.verify_interaction(interaction) - - def verify_interaction(self, interaction): - req = self.build_request(interaction['request']) - response = urllib.request.urlopen(req) - consumer_response = interaction['response'] - - response_headers = response.getheaders() - response_status = response.status - response_reason = response.reason - response_body = json.loads(response.read().decode()) - - status_matches = self.status_matches(consumer_response['status'], response_status) - headers_match = self.headers_match(consumer_response['headers'], response_headers) - body_matches = self.body_matches(consumer_response['body'], response_body) - - return status_matches and headers_match and body_matches - - def body_matches(self, consumer_body, response_body): - return consumer_body.items() <= response_body.items() - - def headers_match(self, consumer_headers, response_headers): - return consumer_headers.items() <= dict(response_headers).items() - - def reason_matches(self, consumer_reason, response_reason): - return consumer_reason == response_reason - - def status_matches(self, consumer_status, response_status): - return consumer_status == response_status - - def build_request(self, consumer_request): - url = urllib.parse.urljoin(self.BASE_URL, consumer_request['path']) - url += consumer_request.get('query', '') - method = consumer_request.get('method', 'GET') - headers = consumer_request.get('headers', {}) - data = consumer_request.get('body', {}) - return Request(url=url, method=method, headers=headers, data=data) - - def load_interactions(self): - return self.fetch_pact_file()['interactions'] - - def fetch_pact_file(self): - pact_uri = read_marker(self.pyfuncitem, PACT_URI) - if pact_uri.startswith(('http://', 'https://')): - with urllib.request.urlopen(pact_uri) as f: - pact = json.load(f) - else: - with open(pact_uri) as f: - pact = json.load(f) - return pact - - def tear_down(self): - self.pact_helper.tear_down() - - def load_pact_helper(self, path_to_pact_helper): - pact_helper = imp.load_source('pact_helper', path_to_pact_helper) - if hasattr(pact_helper, 'set_up') is False: raise Exception(self.SET_UP_NOT_FOUND) - if hasattr(pact_helper, 'tear_down') is False: raise Exception(self.TEAR_DOWN_NOT_FOUND) - return pact_helper - - def pact_helper_path(self): - test_dir = os.path.dirname(self.pyfuncitem.fspath) - files = [f for f in os.listdir(test_dir) if f == self.PACT_HELPER] - if not files: raise Exception(self.PACT_HELPER_NOT_FOUND) - pact_helper_path = os.path.join(test_dir, files[0]) - return pact_helper_path diff --git a/pact_test/executors/standard_executor.py b/pact_test/executors/standard_executor.py deleted file mode 100644 index 10f6fbe..0000000 --- a/pact_test/executors/standard_executor.py +++ /dev/null @@ -1,5 +0,0 @@ -from pact_test.executors.executor import Executor - - -class StandardExecutor(Executor): - pass diff --git a/pact_test/executors/__init__.py b/pact_test/models/__init__.py similarity index 100% rename from pact_test/executors/__init__.py rename to pact_test/models/__init__.py diff --git a/pact_test/models/service_consumer_test.py b/pact_test/models/service_consumer_test.py new file mode 100644 index 0000000..6192985 --- /dev/null +++ b/pact_test/models/service_consumer_test.py @@ -0,0 +1,39 @@ +class ServiceConsumerTest(object): + pact_uri = None + has_pact_with = None + + @property + def states(self): + for attr in dir(self): + obj = getattr(self, attr) + if callable(obj) and hasattr(obj, 'state'): + yield obj + + +def pact_uri(pact_uri_value): + def wrapper(calling_class): + setattr(calling_class, 'set_pact_uri', eval('set_pact_uri(calling_class, "' + pact_uri_value + '")')) + return calling_class + return wrapper + + +def set_pact_uri(self, pact_uri_value): + self.pact_uri = pact_uri_value + + +def has_pact_with(has_pact_with_value): + def wrapper(calling_class): + setattr(calling_class, 'set_has_pact_with', eval('set_has_pact_with(calling_class, "' + has_pact_with_value + '")')) + return calling_class + return wrapper + + +def set_has_pact_with(self, has_pact_with_value): + self.has_pact_with = has_pact_with_value + + +def state(state_value): + def decorate(function): + function.state = state_value + return function + return decorate diff --git a/pact_test/models/service_provider_test.py b/pact_test/models/service_provider_test.py new file mode 100644 index 0000000..4a9cc68 --- /dev/null +++ b/pact_test/models/service_provider_test.py @@ -0,0 +1,67 @@ +class ServiceProviderTest(object): + service_consumer = None + has_pact_with = None + + @property + def decorated_methods(self): + for attr in dir(self): + obj = getattr(self, attr) + check = ( + callable(obj) + and hasattr(obj, 'given') + and hasattr(obj, 'upon_receiving') + and hasattr(obj, 'with_request') + and hasattr(obj, 'will_respond_with') + ) + if check: + yield obj + + +def service_consumer(service_consumer_value): + def wrapper(calling_class): + setattr(calling_class, 'set_service_consumer', eval('set_service_consumer(calling_class, "' + service_consumer_value + '")')) + return calling_class + return wrapper + + +def set_service_consumer(self, service_consumer_value): + self.service_consumer = service_consumer_value + + +def has_pact_with(has_pact_with_value): + def wrapper(calling_class): + setattr(calling_class, 'set_has_pact_with', eval('set_has_pact_with(calling_class, "' + has_pact_with_value + '")')) + return calling_class + return wrapper + + +def set_has_pact_with(self, has_pact_with_value): + self.has_pact_with = has_pact_with_value + + +def given(given_value): + def decorate(function): + function.given = given_value + return function + return decorate + + +def upon_receiving(upon_receiving_value): + def decorate(function): + function.upon_receiving = upon_receiving_value + return function + return decorate + + +def with_request(with_request_value): + def decorate(function): + function.with_request = with_request_value + return function + return decorate + + +def will_respond_with(will_respond_with_value): + def decorate(function): + function.will_respond_with = will_respond_with_value + return function + return decorate diff --git a/pact_test/pact_markers.py b/pact_test/pact_markers.py deleted file mode 100644 index 384aec4..0000000 --- a/pact_test/pact_markers.py +++ /dev/null @@ -1,26 +0,0 @@ -import pytest - - -state = pytest.mark.state -given = pytest.mark.given -base_uri = pytest.mark.base_uri -pact_uri = pytest.mark.pact_uri -with_request = pytest.mark.with_request -has_pact_with = pytest.mark.has_pact_with -upon_receiving = pytest.mark.upon_receiving -will_respond_with = pytest.mark.will_respond_with -service_consumer = pytest.mark.service_consumer -honours_pact_with = pytest.mark.honours_pact_with - -CONSUMER = 'CONSUMER' -PROVIDER = 'PROVIDER' -HAS_PACT_WITH = 'has_pact_with' -HONOURS_PACT_WITH = 'honours_pact_with' -STATE = 'state' -GIVEN = 'given' -BASE_URI = 'base_uri' -PACT_URI = 'pact_uri' -WITH_REQUEST = 'with_request' -UPON_RECEIVING = 'upon_receiving' -WILL_RESPOND_WITH = 'will_respond_with' -SERVICE_CONSUMER = 'service_consumer' diff --git a/pact_test/utils/__init__.py b/pact_test/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pact_test/utils/pact_utils.py b/pact_test/utils/pact_utils.py deleted file mode 100644 index a216c1d..0000000 --- a/pact_test/utils/pact_utils.py +++ /dev/null @@ -1,37 +0,0 @@ -import pytest -from pact_test.utils.pytest_utils import read_marker -from pact_test.pact_markers import CONSUMER -from pact_test.pact_markers import PROVIDER -from pact_test.pact_markers import HAS_PACT_WITH -from pact_test.pact_markers import HONOURS_PACT_WITH -from pact_test.executors.standard_executor import StandardExecutor -from pact_test.executors.consumer_executor import ConsumerExecutor -from pact_test.executors.provider_executor import ProviderExecutor - - -def executor(pyfuncitem): - executor_type = pact_type(pyfuncitem) - executor = None - if executor_type is CONSUMER: - executor = ConsumerExecutor(pyfuncitem) - elif executor_type is PROVIDER: - executor = ProviderExecutor(pyfuncitem) - else: - executor = StandardExecutor(pyfuncitem) - return executor - - -def pact_type(pyfuncitem): - if is_consumer(pyfuncitem): - return CONSUMER - elif is_provider(pyfuncitem): - return PROVIDER - return None - - -def is_consumer(pyfuncitem): - return read_marker(pyfuncitem, HAS_PACT_WITH) is not None - - -def is_provider(pyfuncitem): - return read_marker(pyfuncitem, HONOURS_PACT_WITH) is not None diff --git a/pact_test/utils/pytest_utils.py b/pact_test/utils/pytest_utils.py deleted file mode 100644 index 8fa32f5..0000000 --- a/pact_test/utils/pytest_utils.py +++ /dev/null @@ -1,6 +0,0 @@ -import pytest - - -def read_marker(pyfuncitem, marker_name): - marker = pyfuncitem.get_marker(marker_name) - return marker.args[0] if marker else None diff --git a/tests/executors/__init__.py b/tests/executors/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/executors/test_consumer_executor.py b/tests/executors/test_consumer_executor.py deleted file mode 100644 index aba0988..0000000 --- a/tests/executors/test_consumer_executor.py +++ /dev/null @@ -1,37 +0,0 @@ -from pact_test.executors.consumer_executor import ConsumerExecutor - - -def test_valid_setup(): - class FakeMarker(object): - def __init__(self, marker_name): - self.args = [marker_name + '_VALUE'] - - class FakePyFuncItem(object): - def get_marker(self, marker_name): - return FakeMarker(marker_name) - pyfuncitem = FakePyFuncItem() - assert ConsumerExecutor(pyfuncitem).is_valid() - - -def test_missing_markers(): - class FakeMarker(object): - def __init__(self, marker_name): - self.args = [marker_name + '_VALUE'] - - class FakePyFuncItem(object): - def get_marker(self, marker_name): - return FakeMarker(marker_name) if marker_name is 'state' else None - pyfuncitem = FakePyFuncItem() - assert ConsumerExecutor(pyfuncitem).is_valid() is False - - -def test_null_values(): - class FakeMarker(object): - def __init__(self, marker_name): - self.args = [42] if marker_name is 'state' else [None] - - class FakePyFuncItem(object): - def get_marker(self, marker_name): - return FakeMarker(marker_name) - pyfuncitem = FakePyFuncItem() - assert ConsumerExecutor(pyfuncitem).is_valid() is False diff --git a/tests/executors/test_provider_executor.py b/tests/executors/test_provider_executor.py deleted file mode 100644 index fcb1e2d..0000000 --- a/tests/executors/test_provider_executor.py +++ /dev/null @@ -1,296 +0,0 @@ -import os -import imp -import json -import pytest -import urllib.request -from urllib.request import Request -from pact_test.executors.provider_executor import ProviderExecutor - - -def test_verify_pact(mocker): - class Marker(object): - args = ['a menu exists'] - - class Item(object): - def get_marker(self, name): - return Marker() - - pact_file = simple_pact() - executor = ProviderExecutor(Item()) - mocker.patch.object(executor, 'fetch_pact_file') - mocker.patch.object(executor, 'verify_interaction') - executor.fetch_pact_file.return_value = pact_file - executor.verify_interaction.return_value = True - - assert executor.verify_pact() is True - - -def test_verify_interaction(monkeypatch): - class FakeReader(object): - def decode(self): - return str({ - "id": 42, - "name": "Spam, Eggs and Bacon", - "ingredients": ["spam", "eggs", "bacon"] - }).replace("'", "\"") - - class FakeResponse(object): - status = 200 - reason = 'OK' - - def getheaders(self): - return [ - ("Content-Type", "application/json"), - ("Pragma", "no-cache") - ] - - def read(self): - return FakeReader() - - executor = ProviderExecutor(None) - interaction = simple_pact()['interactions'][0] - - def urlopen(url): - return FakeResponse() - monkeypatch.setattr(urllib.request, 'urlopen', urlopen) - - assert executor.verify_interaction(interaction) is True - - -def test_body_matches(): - executor = ProviderExecutor(None) - consumer_body = {"name": "Spam, Eggs and Bacon"} - response_body = { - "name": "Spam, Eggs and Bacon", - "ingredients": ["spam", "eggs", "bacon"], - "vegan": False, - "vegetarian": False - } - - assert executor.body_matches(consumer_body, response_body) is True - - -def test_headers_match(): - executor = ProviderExecutor(None) - consumer_headers = { - "Content-Type": "application/json", - "Pragma": "no-cache" - } - response_headers = [ - ('Content-Type', 'application/json'), - ('Content-Length', '292'), - ('Access-Control-Allow-Credentials', 'true'), - ('Pragma', 'no-cache') - ] - - assert executor.headers_match(consumer_headers, response_headers) is True - - -def test_reason_matches(): - executor = ProviderExecutor(None) - consumer_reason = "I'm a teapot." - response_reason = "I'm a teapot." - - assert executor.reason_matches(consumer_reason, response_reason) is True - - -def test_status_matches(): - executor = ProviderExecutor(None) - consumer_status = 418 - response_status = 418 - - assert executor.status_matches(consumer_status, response_status) is True - - -def test_build_request(): - executor = ProviderExecutor(None) - consumer_request = { - "method": "POST", - "path": "/menu/42", - "query": "?vegan=false", - "headers": { - "Content-Type": "application/json" - }, - "body": { - "alligator": { - "name": "Mary" - } - } - } - request = executor.build_request(consumer_request) - assert request.get_method() == 'POST' - assert request.full_url == 'http://localhost:1234/menu/42?vegan=false' - assert request.headers == {"Content-type": "application/json"} - assert request.data == {"alligator": {"name": "Mary"}} - - -def test_load_interactions(): - class Marker(object): - args = [os.getcwd() + '/tests/resources/simple_pact.json'] - - class Item(object): - def get_marker(self, name): - return Marker() - - executor = ProviderExecutor(Item()) - expected_interactions = simple_pact()['interactions'] - - assert executor.load_interactions() == expected_interactions - - -def test_fetch_remote_pact_file(monkeypatch): - class Marker(object): - args = ['http://test.com/simple_pact.json'] - - class Item(object): - def get_marker(self, name): - return Marker() - - def url_open(_): - return open(os.getcwd() + '/tests/resources/simple_pact.json') - monkeypatch.setattr(urllib.request, 'urlopen', url_open) - - assert ProviderExecutor(Item()).fetch_pact_file() == simple_pact() - - -def test_fetch_local_pact_file(): - class Marker(object): - args = [os.getcwd() + '/tests/resources/simple_pact.json'] - - class Item(object): - def get_marker(self, name): - return Marker() - - assert ProviderExecutor(Item()).fetch_pact_file() == simple_pact() - - -def test_set_up(mocker): - class PactHelper(object): - def set_up(self): - pass - - pact_helper = PactHelper() - mocker.spy(pact_helper, 'set_up') - - executor = ProviderExecutor(None) - mocker.patch.object(executor, 'set_up', new=pact_helper.set_up) - - executor.set_up() - assert pact_helper.set_up.call_count == 1 - - -def test_tear_down(mocker): - class PactHelper(object): - def tear_down(self): - pass - - pact_helper = PactHelper() - mocker.spy(pact_helper, 'tear_down') - - executor = ProviderExecutor(FakePyFuncItem()) - mocker.patch.object(executor, 'tear_down', new=pact_helper.tear_down) - - executor.set_up() - executor.tear_down() - assert pact_helper.tear_down.call_count == 1 - - -def test_load_pact_helper(): - executor = ProviderExecutor(FakePyFuncItem()) - - helper_path = os.getcwd() + '/tests/resources/pact_helper.py' - pact_helper = executor.load_pact_helper(helper_path) - - assert pact_helper is not None - - -def test_pact_helper_has_no_setup_method(): - executor = ProviderExecutor(FakePyFuncItem()) - - helper_path = os.getcwd() + '/tests/resources/pact_helper_no_setup.py' - - err_msg = 'Module pact_helper MUST have a set_up method.' - try: - executor.load_pact_helper(helper_path) - except Exception as e: - assert str(e) == err_msg - - -def test_pact_helper_has_no_teardown_method(): - executor = ProviderExecutor(FakePyFuncItem()) - - helper_path = os.getcwd() + '/tests/resources/pact_helper_no_teardown.py' - - err_msg = 'Module pact_helper MUST have a tear_down method.' - try: - executor.load_pact_helper(helper_path) - except Exception as e: - assert str(e) == err_msg - - -def test_pact_helper_path(monkeypatch): - executor = ProviderExecutor(FakePyFuncItem()) - - def list_files(dir): - return ['spam.py', 'eggs.py', 'pact_helper.py'] - monkeypatch.setattr(os, 'listdir', list_files) - - helper_path = os.getcwd() + '/tests/resources/pact_helper.py' - assert executor.pact_helper_path() == helper_path - - -def test_pact_helper_not_found(monkeypatch): - executor = ProviderExecutor(FakePyFuncItem()) - - def list_files(dir): - return ['spam.py', 'eggs.py', 'bacon.py'] - monkeypatch.setattr(os, 'listdir', list_files) - - err_msg = 'Could\'n find "pact_helper.py" script in Pact test directory.' - try: - executor.pact_helper_path() - except Exception as e: - assert str(e) == err_msg - - -def test_valid_setup(): - assert ProviderExecutor(FakePyFuncItem()).is_valid() - - -def test_missing_markers(mocker): - def get_marker(name): - return FakeMarker(name, name) if name is 'state' else None - - pyfuncitem = FakePyFuncItem() - mocker.patch.object(pyfuncitem, 'get_marker', new=get_marker) - - assert ProviderExecutor(pyfuncitem).is_valid() is False - - -def test_missing_values(mocker): - def get_marker(name): - return FakeMarker(name, None) if name is 'state' else None - - pyfuncitem = FakePyFuncItem() - mocker.patch.object(pyfuncitem, 'get_marker', new=get_marker) - - assert ProviderExecutor(pyfuncitem).is_valid() is False - - -class FakeMarker(object): - def __init__(self, marker_name, marker_value): - self.args = [marker_name + '_VALUE'] - - -class FakePyFuncItem(object): - fspath = os.getcwd() + '/tests/resources/my_test.py' - - def get_marker(self, marker_name): - return FakeMarker(marker_name, marker_name) - - -def simple_pact(): - path_to_pact = os.getcwd() + '/tests/resources/simple_pact.json' - with open(path_to_pact) as f: - pact = json.load(f) - return pact diff --git a/tests/models/test_service_consumer_test.py b/tests/models/test_service_consumer_test.py new file mode 100644 index 0000000..8f39807 --- /dev/null +++ b/tests/models/test_service_consumer_test.py @@ -0,0 +1,57 @@ +from pact_test.models.service_consumer_test import state +from pact_test.models.service_consumer_test import pact_uri +from pact_test.models.service_consumer_test import has_pact_with +from pact_test.models.service_consumer_test import ServiceConsumerTest + + +def test_default_pact_uri(): + t = ServiceConsumerTest() + assert t.pact_uri is None + + +def test_pact_uri_decorator(): + @pact_uri('http://montypython.com/') + class MyTest(ServiceConsumerTest): + pass + + t = MyTest() + assert t.pact_uri == 'http://montypython.com/' + + +def test_default_has_pact_with(): + t = ServiceConsumerTest() + assert t.has_pact_with is None + + +def test_has_pact_with_decorator(): + @has_pact_with('Library App') + class MyTest(ServiceConsumerTest): + pass + + t = MyTest() + assert t.has_pact_with == 'Library App' + + +def test_decorators(): + @has_pact_with('Library App') + @pact_uri('http://montypython.com/') + class MyTest(ServiceConsumerTest): + pass + + t = MyTest() + assert t.has_pact_with == 'Library App' + assert t.pact_uri == 'http://montypython.com/' + + +def test_states(): + class MyTest(ServiceConsumerTest): + @state('Default state') + def setup(self): + return 42 + + t = MyTest() + default_state = next(t.states) + + assert default_state is not None + assert default_state.state == 'Default state' + assert default_state() == 42 diff --git a/tests/models/test_service_provider_test.py b/tests/models/test_service_provider_test.py new file mode 100644 index 0000000..89770d5 --- /dev/null +++ b/tests/models/test_service_provider_test.py @@ -0,0 +1,80 @@ +from pact_test.models.service_provider_test import given +from pact_test.models.service_provider_test import with_request +from pact_test.models.service_provider_test import has_pact_with +from pact_test.models.service_provider_test import upon_receiving +from pact_test.models.service_provider_test import service_consumer +from pact_test.models.service_provider_test import will_respond_with +from pact_test.models.service_provider_test import ServiceProviderTest + + +def test_default_service_consumer_value(): + t = ServiceProviderTest() + assert t.service_consumer is None + + +def test_decorator_service_consumer_value(): + @service_consumer('Library App') + class MyTest(ServiceProviderTest): + pass + + t = MyTest() + assert t.service_consumer == 'Library App' + + +def test_default_has_pact_with_value(): + t = ServiceProviderTest() + assert t.has_pact_with is None + + +def test_decorator_has_pact_with_value(): + @has_pact_with('Books Service') + class MyTest(ServiceProviderTest): + pass + + t = MyTest() + assert t.has_pact_with == 'Books Service' + + +def test_class_decorators(): + @service_consumer('Library App') + @has_pact_with('Books Service') + class MyTest(ServiceProviderTest): + pass + + t = MyTest() + assert t.service_consumer == 'Library App' + assert t.has_pact_with == 'Books Service' + + +def test_method_decorators(): + class MyTest(ServiceProviderTest): + @given('the breakfast menu is available') + @upon_receiving('a request for a breakfast') + @with_request('I don\'t like spam') + @will_respond_with('Spam & Eggs') + def make_me_breakfast(self): + return 'Spam & Eggs' + + t = MyTest() + decorated_method = next(t.decorated_methods) + + assert decorated_method is not None + assert decorated_method.given == 'the breakfast menu is available' + assert decorated_method.upon_receiving == 'a request for a breakfast' + assert decorated_method.with_request == 'I don\'t like spam' + assert decorated_method.will_respond_with == 'Spam & Eggs' + assert decorated_method() == 'Spam & Eggs' + + +def test_missing_method_decorators(): + class MyTest(ServiceProviderTest): + @given('the breakfast menu is available') + def make_me_breakfast(self): + return 'Spam & Eggs' + + t = MyTest() + try: + next(t.decorated_methods) + assert False + except StopIteration: + assert True diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/utils/test_pact_utils.py b/tests/utils/test_pact_utils.py deleted file mode 100644 index c1e28ac..0000000 --- a/tests/utils/test_pact_utils.py +++ /dev/null @@ -1,82 +0,0 @@ -from pact_test.utils.pact_utils import * - - -def test_standard_executor(mocker): - pyfuncitem = FakePyFuncItem() - mocker.patch.object(pyfuncitem, 'get_marker') - pyfuncitem.get_marker.return_value = None - - assert type(executor(pyfuncitem)).__name__ is 'StandardExecutor' - - -def test_consumer_executor(mocker): - def get_marker(marker_name): - return pymarker if marker_name == HAS_PACT_WITH else None - - pyfuncitem = FakePyFuncItem() - mocker.patch.object(pyfuncitem, 'get_marker', new=get_marker) - - pymarker = FakeMarker() - mocker.patch.object(pymarker, 'args') - pymarker.args.return_value = ['Books Service'] - - assert type(executor(pyfuncitem)).__name__ is 'ConsumerExecutor' - - -def test_provider_executor(mocker): - def get_marker(marker_name): - return pymarker if marker_name == HONOURS_PACT_WITH else None - - pyfuncitem = FakePyFuncItem() - mocker.patch.object(pyfuncitem, 'get_marker', new=get_marker) - - pymarker = FakeMarker() - mocker.patch.object(pymarker, 'args') - pymarker.args.return_value = ['Hallo world!'] - - assert type(executor(pyfuncitem)).__name__ is 'ProviderExecutor' - - -def test_is_standard_test(mocker): - pyfuncitem = FakePyFuncItem() - mocker.patch.object(pyfuncitem, 'get_marker') - pyfuncitem.get_marker.return_value = None - - assert pact_type(pyfuncitem) is None - - -def test_is_consumer_test(mocker): - def get_marker(marker_name): - return pymarker if marker_name == HAS_PACT_WITH else None - - pyfuncitem = FakePyFuncItem() - mocker.patch.object(pyfuncitem, 'get_marker', new=get_marker) - - pymarker = FakeMarker() - mocker.patch.object(pymarker, 'args') - pymarker.args.return_value = ['Hallo world!'] - - assert pact_type(pyfuncitem) == CONSUMER - - -def test_is_provider_test(mocker): - def get_marker(marker_name): - return pymarker if marker_name == HONOURS_PACT_WITH else None - - pyfuncitem = FakePyFuncItem() - mocker.patch.object(pyfuncitem, 'get_marker', new=get_marker) - - pymarker = FakeMarker() - mocker.patch.object(pymarker, 'args') - pymarker.args.return_value = ['Hallo world!'] - - assert pact_type(pyfuncitem) == PROVIDER - - -class FakePyFuncItem(object): - def get_marker(self, marker_name): - return None - - -class FakeMarker(object): - args = [] diff --git a/tests/utils/test_pytest_utils.py b/tests/utils/test_pytest_utils.py deleted file mode 100644 index 75215a6..0000000 --- a/tests/utils/test_pytest_utils.py +++ /dev/null @@ -1,23 +0,0 @@ -from pact_test.utils.pytest_utils import * - - -def test_read_existing_marker(): - class FakeMarker(object): - args = ['My Value'] - - class FakePyFuncItem(object): - def get_marker(self, marker_name): - return FakeMarker() - - pyfuncitem = FakePyFuncItem() - assert read_marker(pyfuncitem, 'My Marker') == 'My Value' - - -def test_read_non_existing_marker(): - - class FakePyFuncItem(object): - def get_marker(self, marker_name): - return None - - pyfuncitem = FakePyFuncItem() - assert read_marker(pyfuncitem, 'My Marker') is None diff --git a/tox.ini b/tox.ini index 4c067f7..34871a6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] -# envlist = py27, py33, py34, py35, py36 -envlist = py36 +#envlist = py27, py33, py34, py35, py36 +envlist = py27, py36 [testenv] deps = From 770cbb092da521b236e7133e6920e5f0e7db4ad8 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 27 May 2017 14:17:42 +1000 Subject: [PATCH 13/85] Validation and setup of service consumer tests. --- .gitignore | 2 + .travis.yml | 3 + README.rst | 17 ++-- bin/pact-test | 41 +++++---- pact.py | 15 ---- pact_test/config/__init__.py | 0 pact_test/config/config_builder.py | 25 ++++++ pact_test/exceptions/__init__.py | 2 + pact_test/models/service_consumer_test.py | 15 +++- pact_test/models/service_provider_test.py | 6 +- pact_test/runners/__init__.py | 0 pact_test/runners/consumer_tests_runner.py | 54 ++++++++++++ pact_test/runners/pact_tests_runner.py | 19 +++++ pact_test/runners/provider_tests_runner.py | 6 ++ setup.cfg | 3 +- tests/config/test_config_builder.py | 69 +++++++++++++++ tests/exceptions/test_pact_exception.py | 8 ++ tests/models/test_service_consumer_test.py | 26 ++++++ tests/resources/config/.pact.json | 5 ++ tests/resources/config/consumer_only.json | 3 + tests/resources/config/pact_broker_only.json | 3 + tests/resources/config/provider_only.json | 3 + .../{ => pact_helper}/pact_helper.py | 0 .../pact_helper.py} | 0 .../pact_helper.py} | 2 +- .../service_consumers/pact_helper.py | 0 .../test_restaurant_customer.py | 10 +++ tests/runners/test_consumer_tests_runner.py | 85 +++++++++++++++++++ tests/runners/test_pact_tests_runner.py | 21 +++++ 29 files changed, 402 insertions(+), 41 deletions(-) mode change 100644 => 100755 bin/pact-test delete mode 100644 pact.py create mode 100644 pact_test/config/__init__.py create mode 100644 pact_test/config/config_builder.py create mode 100644 pact_test/exceptions/__init__.py create mode 100644 pact_test/runners/__init__.py create mode 100644 pact_test/runners/consumer_tests_runner.py create mode 100644 pact_test/runners/pact_tests_runner.py create mode 100644 pact_test/runners/provider_tests_runner.py create mode 100644 tests/config/test_config_builder.py create mode 100644 tests/exceptions/test_pact_exception.py create mode 100644 tests/resources/config/.pact.json create mode 100644 tests/resources/config/consumer_only.json create mode 100644 tests/resources/config/pact_broker_only.json create mode 100644 tests/resources/config/provider_only.json rename tests/resources/{ => pact_helper}/pact_helper.py (100%) rename tests/resources/{pact_helper_no_setup.py => pact_helper_no_setup/pact_helper.py} (100%) rename tests/resources/{pact_helper_no_teardown.py => pact_helper_no_tear_down/pact_helper.py} (70%) create mode 100644 tests/resources/service_consumers/pact_helper.py create mode 100644 tests/resources/service_consumers/test_restaurant_customer.py create mode 100644 tests/runners/test_consumer_tests_runner.py create mode 100644 tests/runners/test_pact_tests_runner.py diff --git a/.gitignore b/.gitignore index 9b62e8b..31d93a5 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ __pycache__ pytest_pact.egg-info/ dist/ .tox +.idea/ +*.iml diff --git a/.travis.yml b/.travis.yml index a21679c..fd00d86 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,9 @@ language: python sudo: false python: + - 2.7 + - 3.3 + - 3.4 - 3.5 - 3.6 diff --git a/README.rst b/README.rst index 2f965f8..3e291ee 100644 --- a/README.rst +++ b/README.rst @@ -1,14 +1,21 @@ -.. image:: https://travis-ci.org/Kalimaha/pytest-pact.svg?branch=master - :target: https://travis-ci.org/Kalimaha/pytest-pact +.. image:: https://travis-ci.org/Kalimaha/pact-test.svg?branch=master + :target: https://travis-ci.org/Kalimaha/pact-test .. image:: https://coveralls.io/repos/github/Kalimaha/pact-test/badge.svg?branch=development :target: https://coveralls.io/github/Kalimaha/pact-test?branch=development +.. image:: https://img.shields.io/badge/python-2.7-blue.svg + :target: https://travis-ci.org/Kalimaha/pact-test +.. image:: https://img.shields.io/badge/python-3.3-blue.svg + :target: https://travis-ci.org/Kalimaha/pact-test +.. image:: https://img.shields.io/badge/python-3.4-blue.svg + :target: https://travis-ci.org/Kalimaha/pact-test .. image:: https://img.shields.io/badge/python-3.5-blue.svg - :target: https://travis-ci.org/Kalimaha/pytest-pact + :target: https://travis-ci.org/Kalimaha/pact-test .. image:: https://img.shields.io/badge/python-3.6-blue.svg - :target: https://travis-ci.org/Kalimaha/pytest-pact + :target: https://travis-ci.org/Kalimaha/pact-test -Pact for PyTest +Pact Test for Python =============== + Python implementation for Pact (http://pact.io/) Setup diff --git a/bin/pact-test b/bin/pact-test old mode 100644 new mode 100755 index fbb0d17..48a897d --- a/bin/pact-test +++ b/bin/pact-test @@ -1,27 +1,38 @@ #!/usr/bin/env python +import os import sys +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) +from pact_test.runners.pact_tests_runner import verify def print_help(): - print(' ') - print('================================') - print('Welcome to Pact Test for Python!') - print('--------------------------------') - print(' Available Commands ') - print(' ') - print('verify..........Verify Pact test') - print('================================') - print(' ') - - -def verify(): - pass + print(' ') + print('========================================================') + print(' Welcome to Pact Test for Python! ') + print('--------------------------------------------------------') + print(' Available Commands ') + print(' ') + print('help..............................Display this help page') + print('verify.............................Verify all Pact tests') + print('verify consumers..............Verify Consumer Pact tests') + print('verify providers..............Verify Provider Pact tests') + print('========================================================') + print(' ') if len(sys.argv) is 1: print_help() else: - if sys.argv[1] == 'verify': - verify() + if sys.argv[1].lower() == 'verify': + if len(sys.argv) is 2: + verify(verify_consumers=True, verify_providers=True) + elif sys.argv[2].lower() == 'consumers': + verify(verify_consumers=True, verify_providers=False) + elif sys.argv[2].lower() == 'providers': + verify(verify_consumers=False, verify_providers=True) + else: + print_help() + elif sys.argv[1].lower() == 'help': + print_help() else: print_help() diff --git a/pact.py b/pact.py deleted file mode 100644 index b6cd84a..0000000 --- a/pact.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest -from pytest_pact.utils.pact_utils import * -from pytest_pact.utils.pytest_utils import read_marker -from pytest_pact.pact_markers import * - - -@pytest.hookimpl(hookwrapper=True) -def pytest_pyfunc_call(pyfuncitem): - test_executor = executor(pyfuncitem) - print('=============================') - print(test_executor) - test_executor.set_up() - outcome = yield - res = outcome.get_result() - test_executor.tear_down() diff --git a/pact_test/config/__init__.py b/pact_test/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pact_test/config/config_builder.py b/pact_test/config/config_builder.py new file mode 100644 index 0000000..5a0547f --- /dev/null +++ b/pact_test/config/config_builder.py @@ -0,0 +1,25 @@ +import os +import json + + +class Config(object): + pact_broker_uri = None + consumer_tests_path = 'tests/service_consumers' + provider_tests_path = 'tests/service_providers' + CONFIGURATION_FILE = '.pact.json' + + def __init__(self): + user_config_file = self.path_to_user_config_file() + if os.path.isfile(user_config_file): + with open(user_config_file) as f: + self.load_user_settings(f) + + def load_user_settings(self, user_settings_file): + settings = json.loads(user_settings_file.read()) + [self.custom_or_default(settings, key) for key in settings.keys()] + + def path_to_user_config_file(self): + return os.path.join(os.getcwd(), self.CONFIGURATION_FILE) + + def custom_or_default(self, user_settings, key): + setattr(self, key, user_settings.get(key, getattr(self, key))) diff --git a/pact_test/exceptions/__init__.py b/pact_test/exceptions/__init__.py new file mode 100644 index 0000000..175625d --- /dev/null +++ b/pact_test/exceptions/__init__.py @@ -0,0 +1,2 @@ +class PactTestException(Exception): + pass diff --git a/pact_test/models/service_consumer_test.py b/pact_test/models/service_consumer_test.py index 6192985..ddbb5bc 100644 --- a/pact_test/models/service_consumer_test.py +++ b/pact_test/models/service_consumer_test.py @@ -1,3 +1,6 @@ +from pact_test.exceptions import PactTestException + + class ServiceConsumerTest(object): pact_uri = None has_pact_with = None @@ -9,10 +12,17 @@ def states(self): if callable(obj) and hasattr(obj, 'state'): yield obj + def is_valid(self): + if self.pact_uri is None: + raise PactTestException('Missing setup for "pact_uri".') + if self.has_pact_with is None: + raise PactTestException('Missing setup for "has_pact_with".') + def pact_uri(pact_uri_value): def wrapper(calling_class): - setattr(calling_class, 'set_pact_uri', eval('set_pact_uri(calling_class, "' + pact_uri_value + '")')) + setattr(calling_class, 'set_pact_uri', + eval('set_pact_uri(calling_class, "' + pact_uri_value + '")')) return calling_class return wrapper @@ -23,7 +33,8 @@ def set_pact_uri(self, pact_uri_value): def has_pact_with(has_pact_with_value): def wrapper(calling_class): - setattr(calling_class, 'set_has_pact_with', eval('set_has_pact_with(calling_class, "' + has_pact_with_value + '")')) + setattr(calling_class, 'set_has_pact_with', + eval('set_has_pact_with(calling_class, "' + has_pact_with_value + '")')) return calling_class return wrapper diff --git a/pact_test/models/service_provider_test.py b/pact_test/models/service_provider_test.py index 4a9cc68..0066d5a 100644 --- a/pact_test/models/service_provider_test.py +++ b/pact_test/models/service_provider_test.py @@ -19,7 +19,8 @@ def decorated_methods(self): def service_consumer(service_consumer_value): def wrapper(calling_class): - setattr(calling_class, 'set_service_consumer', eval('set_service_consumer(calling_class, "' + service_consumer_value + '")')) + setattr(calling_class, 'set_service_consumer', + eval('set_service_consumer(calling_class, "' + service_consumer_value + '")')) return calling_class return wrapper @@ -30,7 +31,8 @@ def set_service_consumer(self, service_consumer_value): def has_pact_with(has_pact_with_value): def wrapper(calling_class): - setattr(calling_class, 'set_has_pact_with', eval('set_has_pact_with(calling_class, "' + has_pact_with_value + '")')) + setattr(calling_class, 'set_has_pact_with', + eval('set_has_pact_with(calling_class, "' + has_pact_with_value + '")')) return calling_class return wrapper diff --git a/pact_test/runners/__init__.py b/pact_test/runners/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pact_test/runners/consumer_tests_runner.py b/pact_test/runners/consumer_tests_runner.py new file mode 100644 index 0000000..bd91789 --- /dev/null +++ b/pact_test/runners/consumer_tests_runner.py @@ -0,0 +1,54 @@ +import os +import imp +import inspect +from pact_test.exceptions import PactTestException + + +class ConsumerTestsRunner(object): + pact_helper = None + + def __init__(self, config): + self.config = config + + def verify(self): + pass + + def collect_tests(self): + root = self.config.consumer_tests_path + files = list(filter(filter_rule, self.all_files())) + files = list(map(lambda f: os.path.join(root, f), files)) + tests = [] + for idx, filename in enumerate(files): + test = imp.load_source('test' + str(idx), filename) + for name, obj in inspect.getmembers(test): + if inspect.isclass(obj) and len(inspect.getmro(obj)) > 2: + test_parent = inspect.getmro(obj)[1].__name__ + if test_parent == 'ServiceConsumerTest': + tests.append(obj) + + if not files: + raise PactTestException('There are no consumer tests to verify.') + return tests + + def all_files(self): + return os.listdir(self.config.consumer_tests_path) + + def load_pact_helper(self): + self.pact_helper = imp.load_source('pact_helper', self.path_to_pact_helper()) + if hasattr(self.pact_helper, 'setup') is False: + raise PactTestException('Missing "setup" method in "pact_helper.py".') + if hasattr(self.pact_helper, 'tear_down') is False: + raise PactTestException('Missing "tear_down" method in "pact_helper.py".') + + def path_to_pact_helper(self): + path = os.path.join(self.config.consumer_tests_path, 'pact_helper.py') + if os.path.isfile(path) is False: + msg = 'Missing "pact_helper.py" at "' + self.config.consumer_tests_path + '".' + raise PactTestException(msg) + return path + + +def filter_rule(filename): + return (filename != '__init__.py' and + filename.endswith('.py') and + filename.endswith('pact_helper.py') is False) diff --git a/pact_test/runners/pact_tests_runner.py b/pact_test/runners/pact_tests_runner.py new file mode 100644 index 0000000..ea279e4 --- /dev/null +++ b/pact_test/runners/pact_tests_runner.py @@ -0,0 +1,19 @@ +from pact_test.config.config_builder import Config +from pact_test.runners.consumer_tests_runner import ConsumerTestsRunner +from pact_test.runners.provider_tests_runner import ProviderTestsRunner + + +def verify(verify_consumers=False, verify_providers=False): + config = Config() + if verify_consumers: + run_consumer_tests(config) + if verify_providers: + run_provider_tests(config) + + +def run_consumer_tests(config): + ConsumerTestsRunner(config).verify() + + +def run_provider_tests(config): + ProviderTestsRunner(config).verify() diff --git a/pact_test/runners/provider_tests_runner.py b/pact_test/runners/provider_tests_runner.py new file mode 100644 index 0000000..771fa80 --- /dev/null +++ b/pact_test/runners/provider_tests_runner.py @@ -0,0 +1,6 @@ +class ProviderTestsRunner(object): + def __init__(self, config): + self.config = config + + def verify(self): + pass diff --git a/setup.cfg b/setup.cfg index 42ee8da..1a295af 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,6 +5,7 @@ universal=1 test=pytest [tool:pytest] -addopts = --pep8 +addopts=--pep8 testpaths=tests python_files=*.py +norecursedirs=tests/resources diff --git a/tests/config/test_config_builder.py b/tests/config/test_config_builder.py new file mode 100644 index 0000000..30be1cf --- /dev/null +++ b/tests/config/test_config_builder.py @@ -0,0 +1,69 @@ +import os +from pact_test.config.config_builder import Config + + +def test_default_consumer_tests_path(): + config = Config() + assert config.consumer_tests_path == 'tests/service_consumers' + + +def test_default_provider_tests_path(): + config = Config() + assert config.provider_tests_path == 'tests/service_providers' + + +def test_default_pact_broker_uri(): + config = Config() + assert config.pact_broker_uri is None + + +def test_custom_consumer_tests_path(): + class TestConfig(Config): + def path_to_user_config_file(self): + return os.path.join(os.getcwd(), 'tests', + 'resources', 'config', + 'consumer_only.json') + + config = TestConfig() + assert config.pact_broker_uri is None + assert config.consumer_tests_path == 'mypath/mytests' + assert config.provider_tests_path == 'tests/service_providers' + + +def test_custom_provider_tests_path(): + class TestConfig(Config): + def path_to_user_config_file(self): + return os.path.join(os.getcwd(), 'tests', + 'resources', 'config', + 'provider_only.json') + + config = TestConfig() + assert config.pact_broker_uri is None + assert config.provider_tests_path == 'mypath/mytests' + assert config.consumer_tests_path == 'tests/service_consumers' + + +def test_custom_pact_broker_uri(): + class TestConfig(Config): + def path_to_user_config_file(self): + return os.path.join(os.getcwd(), 'tests', + 'resources', 'config', + 'pact_broker_only.json') + + config = TestConfig() + assert config.pact_broker_uri == 'mypath/mytests' + assert config.provider_tests_path == 'tests/service_providers' + assert config.consumer_tests_path == 'tests/service_consumers' + + +def test_user_settings(): + class TestConfig(Config): + def path_to_user_config_file(self): + return os.path.join(os.getcwd(), 'tests', + 'resources', 'config', + '.pact.json') + + config = TestConfig() + assert config.pact_broker_uri == 'mypath/mybroker' + assert config.provider_tests_path == 'mypath/myprovider' + assert config.consumer_tests_path == 'mypath/myconsumer' diff --git a/tests/exceptions/test_pact_exception.py b/tests/exceptions/test_pact_exception.py new file mode 100644 index 0000000..d6ea46d --- /dev/null +++ b/tests/exceptions/test_pact_exception.py @@ -0,0 +1,8 @@ +from pact_test.exceptions import PactTestException + + +def test_message(): + try: + raise PactTestException('Pact Error') + except PactTestException as e: + assert str(e) == 'Pact Error' diff --git a/tests/models/test_service_consumer_test.py b/tests/models/test_service_consumer_test.py index 8f39807..d51b181 100644 --- a/tests/models/test_service_consumer_test.py +++ b/tests/models/test_service_consumer_test.py @@ -1,3 +1,5 @@ +import pytest +from pact_test.exceptions import PactTestException from pact_test.models.service_consumer_test import state from pact_test.models.service_consumer_test import pact_uri from pact_test.models.service_consumer_test import has_pact_with @@ -55,3 +57,27 @@ def setup(self): assert default_state is not None assert default_state.state == 'Default state' assert default_state() == 42 + + +def test_missing_pact_uri(): + @has_pact_with('Restaurant Customer') + class MyTest(ServiceConsumerTest): + @state('the breakfast is available') + def setup(self): + return 42 + + with pytest.raises(PactTestException) as e: + MyTest().is_valid() + assert str(e.value) == 'Missing setup for "pact_uri".' + + +def test_missing_has_pact_with(): + @pact_uri('http://montypython.com/') + class MyTest(ServiceConsumerTest): + @state('the breakfast is available') + def setup(self): + return 42 + + with pytest.raises(PactTestException) as e: + MyTest().is_valid() + assert str(e.value) == 'Missing setup for "has_pact_with".' diff --git a/tests/resources/config/.pact.json b/tests/resources/config/.pact.json new file mode 100644 index 0000000..c767aa8 --- /dev/null +++ b/tests/resources/config/.pact.json @@ -0,0 +1,5 @@ +{ + "consumer_tests_path": "mypath/myconsumer", + "provider_tests_path": "mypath/myprovider", + "pact_broker_uri": "mypath/mybroker" +} diff --git a/tests/resources/config/consumer_only.json b/tests/resources/config/consumer_only.json new file mode 100644 index 0000000..d2c6f39 --- /dev/null +++ b/tests/resources/config/consumer_only.json @@ -0,0 +1,3 @@ +{ + "consumer_tests_path": "mypath/mytests" +} diff --git a/tests/resources/config/pact_broker_only.json b/tests/resources/config/pact_broker_only.json new file mode 100644 index 0000000..4ec32e1 --- /dev/null +++ b/tests/resources/config/pact_broker_only.json @@ -0,0 +1,3 @@ +{ + "pact_broker_uri": "mypath/mytests" +} diff --git a/tests/resources/config/provider_only.json b/tests/resources/config/provider_only.json new file mode 100644 index 0000000..c61c50a --- /dev/null +++ b/tests/resources/config/provider_only.json @@ -0,0 +1,3 @@ +{ + "provider_tests_path": "mypath/mytests" +} diff --git a/tests/resources/pact_helper.py b/tests/resources/pact_helper/pact_helper.py similarity index 100% rename from tests/resources/pact_helper.py rename to tests/resources/pact_helper/pact_helper.py diff --git a/tests/resources/pact_helper_no_setup.py b/tests/resources/pact_helper_no_setup/pact_helper.py similarity index 100% rename from tests/resources/pact_helper_no_setup.py rename to tests/resources/pact_helper_no_setup/pact_helper.py diff --git a/tests/resources/pact_helper_no_teardown.py b/tests/resources/pact_helper_no_tear_down/pact_helper.py similarity index 70% rename from tests/resources/pact_helper_no_teardown.py rename to tests/resources/pact_helper_no_tear_down/pact_helper.py index 2053adc..e00e960 100644 --- a/tests/resources/pact_helper_no_teardown.py +++ b/tests/resources/pact_helper_no_tear_down/pact_helper.py @@ -1,2 +1,2 @@ -def set_up(): +def setup(): print('Starting provider...') diff --git a/tests/resources/service_consumers/pact_helper.py b/tests/resources/service_consumers/pact_helper.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/resources/service_consumers/test_restaurant_customer.py b/tests/resources/service_consumers/test_restaurant_customer.py new file mode 100644 index 0000000..ecd38ce --- /dev/null +++ b/tests/resources/service_consumers/test_restaurant_customer.py @@ -0,0 +1,10 @@ +from pact_test.models.service_consumer_test import * + + +@has_pact_with('Restaurant') +@pact_uri('http://google.com/') +class TestRestaurantCustomer(ServiceConsumerTest): + + @state('the breakfast is available') + def test_get_breakfast(self): + return 'Spam & Eggs' diff --git a/tests/runners/test_consumer_tests_runner.py b/tests/runners/test_consumer_tests_runner.py new file mode 100644 index 0000000..4647d2e --- /dev/null +++ b/tests/runners/test_consumer_tests_runner.py @@ -0,0 +1,85 @@ +import os +import sys +import pytest +from pact_test.runners.consumer_tests_runner import ConsumerTestsRunner +from pact_test.config.config_builder import Config +from pact_test.exceptions import PactTestException + + +def test_missing_pact_helper(): + config = Config() + t = ConsumerTestsRunner(config) + msg = 'Missing "pact_helper.py" at "tests/service_consumers".' + with pytest.raises(PactTestException) as e: + t.path_to_pact_helper() + assert str(e.value) == msg + + +def test_missing_setup_method(): + remove_pact_helper() + + config = Config() + test_pact_helper_path = os.path.join(os.getcwd(), 'tests', + 'resources', 'pact_helper_no_setup') + config.consumer_tests_path = test_pact_helper_path + t = ConsumerTestsRunner(config) + msg = 'Missing "setup" method in "pact_helper.py".' + with pytest.raises(PactTestException) as e: + t.load_pact_helper() + assert str(e.value) == msg + + +def test_missing_tear_down_method(): + remove_pact_helper() + + config = Config() + test_pact_helper_path = os.path.join(os.getcwd(), 'tests', 'resources', + 'pact_helper_no_tear_down') + config.consumer_tests_path = test_pact_helper_path + t = ConsumerTestsRunner(config) + msg = 'Missing "tear_down" method in "pact_helper.py".' + with pytest.raises(PactTestException) as e: + t.load_pact_helper() + assert str(e.value) == msg + + +def test_empty_tests_list(monkeypatch): + config = Config() + test_pact_helper_path = os.path.join(os.getcwd(), 'tests', 'resources', + 'service_consumers') + config.consumer_tests_path = test_pact_helper_path + + def empty_list(_): + return [] + monkeypatch.setattr(os, 'listdir', empty_list) + + t = ConsumerTestsRunner(config) + with pytest.raises(PactTestException) as e: + t.collect_tests() + assert str(e.value) == 'There are no consumer tests to verify.' + + +def test_collect_tests(): + config = Config() + test_pact_helper_path = os.path.join(os.getcwd(), 'tests', 'resources', + 'service_consumers') + config.consumer_tests_path = test_pact_helper_path + t = ConsumerTestsRunner(config) + + tests = t.collect_tests() + assert len(tests) == 1 + + test = tests[0]() + assert test.pact_uri == 'http://google.com/' + assert test.has_pact_with == 'Restaurant' + + state = next(test.states) + assert state.state == 'the breakfast is available' + assert state() == 'Spam & Eggs' + + +def remove_pact_helper(): + try: + del sys.modules['pact_helper'] + except KeyError: + pass diff --git a/tests/runners/test_pact_tests_runner.py b/tests/runners/test_pact_tests_runner.py new file mode 100644 index 0000000..031238d --- /dev/null +++ b/tests/runners/test_pact_tests_runner.py @@ -0,0 +1,21 @@ +from pact_test.runners import pact_tests_runner + + +def test_consumer_tests(mocker): + mocker.spy(pact_tests_runner, 'run_consumer_tests') + pact_tests_runner.verify(verify_consumers=True) + assert pact_tests_runner.run_consumer_tests.call_count == 1 + + +def test_provider_tests(mocker): + mocker.spy(pact_tests_runner, 'run_provider_tests') + pact_tests_runner.verify(verify_providers=True) + assert pact_tests_runner.run_provider_tests.call_count == 1 + + +def test_default_setup(mocker): + mocker.spy(pact_tests_runner, 'run_consumer_tests') + mocker.spy(pact_tests_runner, 'run_provider_tests') + pact_tests_runner.verify() + assert pact_tests_runner.run_consumer_tests.call_count == 0 + assert pact_tests_runner.run_provider_tests.call_count == 0 From ad22452b47729538dd642fba08a57c8db737822e Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sun, 28 May 2017 13:36:46 +1000 Subject: [PATCH 14/85] WIP: service consumers test --- README.rst | 18 +++---- pact_test/constants/__init__.py | 7 +++ pact_test/either/__init__.py | 20 +++++++ pact_test/models/service_consumer_test.py | 9 ++-- pact_test/runners/consumer_tests_runner.py | 40 ++++++++++++-- pact_test/utils/__init__.py | 0 pact_test/utils/pact_utils.py | 24 +++++++++ tests/either/test_either.py | 47 ++++++++++++++++ tests/models/test_service_consumer_test.py | 9 ++-- .../invalid_service_consumer/customer.py | 9 ++++ tests/resources/pact_files/file.json | 3 ++ tests/resources/pact_files/simple.json | 34 ++++++++++++ tests/runners/test_consumer_tests_runner.py | 54 ++++++++++++++++++- tests/utils/test_pact_utils.py | 44 +++++++++++++++ 14 files changed, 296 insertions(+), 22 deletions(-) create mode 100644 pact_test/constants/__init__.py create mode 100644 pact_test/either/__init__.py create mode 100644 pact_test/utils/__init__.py create mode 100644 pact_test/utils/pact_utils.py create mode 100644 tests/either/test_either.py create mode 100644 tests/resources/invalid_service_consumer/customer.py create mode 100644 tests/resources/pact_files/file.json create mode 100644 tests/resources/pact_files/simple.json create mode 100644 tests/utils/test_pact_utils.py diff --git a/README.rst b/README.rst index 3e291ee..cc9ab97 100644 --- a/README.rst +++ b/README.rst @@ -1,17 +1,15 @@ +.. image:: https://img.shields.io/badge/license-MIT-brightgreen.svg + :target: https://github.com/Kalimaha/pact-test/blob/master/LICENSE +.. image:: https://img.shields.io/badge/python-2.7,%203.3,%203.4,%203.5,%203.6-brightgreen.svg + :target: https://travis-ci.org/Kalimaha/pact-test +.. image:: https://img.shields.io/badge/pypi-0.1.1-brightgreen.svg + :target: https://pypi.python.org/pypi?:action=display&name=pact-test&version=0.1.1 +.. image:: https://img.shields.io/pypi/wheel/Django.svg + :target: https://pypi.python.org/pypi?:action=display&name=pact-test&version=0.1.1 .. image:: https://travis-ci.org/Kalimaha/pact-test.svg?branch=master :target: https://travis-ci.org/Kalimaha/pact-test .. image:: https://coveralls.io/repos/github/Kalimaha/pact-test/badge.svg?branch=development :target: https://coveralls.io/github/Kalimaha/pact-test?branch=development -.. image:: https://img.shields.io/badge/python-2.7-blue.svg - :target: https://travis-ci.org/Kalimaha/pact-test -.. image:: https://img.shields.io/badge/python-3.3-blue.svg - :target: https://travis-ci.org/Kalimaha/pact-test -.. image:: https://img.shields.io/badge/python-3.4-blue.svg - :target: https://travis-ci.org/Kalimaha/pact-test -.. image:: https://img.shields.io/badge/python-3.5-blue.svg - :target: https://travis-ci.org/Kalimaha/pact-test -.. image:: https://img.shields.io/badge/python-3.6-blue.svg - :target: https://travis-ci.org/Kalimaha/pact-test Pact Test for Python =============== diff --git a/pact_test/constants/__init__.py b/pact_test/constants/__init__.py new file mode 100644 index 0000000..6fa28c2 --- /dev/null +++ b/pact_test/constants/__init__.py @@ -0,0 +1,7 @@ +MISSING_STATE = 'Missing implementation for state ' +MISSING_PACT_HELPER = 'Missing "pact_helper.py" at "' +MISSING_PACT_URI = 'Missing setup for "pact_uri" at ' +MISSING_TESTS = 'There are no consumer tests to verify.' +MISSING_SETUP = 'Missing "setup" method in "pact_helper.py".' +MISSING_HAS_PACT_WITH = 'Missing setup for "has_pact_with" at ' +MISSING_TEAR_DOWN = 'Missing "tear_down" method in "pact_helper.py".' diff --git a/pact_test/either/__init__.py b/pact_test/either/__init__.py new file mode 100644 index 0000000..0764ac8 --- /dev/null +++ b/pact_test/either/__init__.py @@ -0,0 +1,20 @@ +__all__ = ['Either', 'Left', 'Right'] + + +class Either(object): + def __init__(self, value): + self.value = value + + def concat(self, f, *args): + if type(self) is Right: + return f(self.value, *args) + else: + return self + + +class Left(Either): + pass + + +class Right(Either): + pass diff --git a/pact_test/models/service_consumer_test.py b/pact_test/models/service_consumer_test.py index ddbb5bc..05b114f 100644 --- a/pact_test/models/service_consumer_test.py +++ b/pact_test/models/service_consumer_test.py @@ -1,3 +1,4 @@ +from pact_test.constants import * from pact_test.exceptions import PactTestException @@ -12,11 +13,13 @@ def states(self): if callable(obj) and hasattr(obj, 'state'): yield obj - def is_valid(self): + def validate(self): if self.pact_uri is None: - raise PactTestException('Missing setup for "pact_uri".') + msg = MISSING_PACT_URI + __file__ + raise PactTestException(msg) if self.has_pact_with is None: - raise PactTestException('Missing setup for "has_pact_with".') + msg = MISSING_HAS_PACT_WITH + __file__ + raise PactTestException(msg) def pact_uri(pact_uri_value): diff --git a/pact_test/runners/consumer_tests_runner.py b/pact_test/runners/consumer_tests_runner.py index bd91789..0e5a423 100644 --- a/pact_test/runners/consumer_tests_runner.py +++ b/pact_test/runners/consumer_tests_runner.py @@ -1,7 +1,9 @@ import os import imp import inspect +from pact_test.constants import * from pact_test.exceptions import PactTestException +from pact_test.utils.pact_utils import get_pact class ConsumerTestsRunner(object): @@ -13,6 +15,36 @@ def __init__(self, config): def verify(self): pass + def verify_test(self, test_class): + test = test_class() + test.validate() + pact = self.get_pact(test.pact_uri) + + pact_states = list(map(lambda i: i['providerState'], pact['interactions'])) + test_states = list(map(lambda s: s.state, test.states)) + + for pact_state in pact_states: + if pact_state not in test_states: + msg = MISSING_STATE + '"' + pact_state + '".' + raise PactTestException(msg) + self.verify_state(test.states, pact_state) + + def verify_state(self, states, provider_state): + # for s in states: + # print('\t' + s.state) + # print(providerState) + # print('PACT HELPER SETUP') + # print('EXECUTE STATE') + # print('CREATE REQUEST') + # print('EXECUTE REQUEST') + # print('VERIFY RESPONSE') + # print('PACT HELPER TEAR DOWN') + pass + + @staticmethod + def get_pact(location): # pragma: no cover + return get_pact(location) # pragma: no cover + def collect_tests(self): root = self.config.consumer_tests_path files = list(filter(filter_rule, self.all_files())) @@ -27,7 +59,7 @@ def collect_tests(self): tests.append(obj) if not files: - raise PactTestException('There are no consumer tests to verify.') + raise PactTestException(MISSING_TESTS) return tests def all_files(self): @@ -36,14 +68,14 @@ def all_files(self): def load_pact_helper(self): self.pact_helper = imp.load_source('pact_helper', self.path_to_pact_helper()) if hasattr(self.pact_helper, 'setup') is False: - raise PactTestException('Missing "setup" method in "pact_helper.py".') + raise PactTestException(MISSING_SETUP) if hasattr(self.pact_helper, 'tear_down') is False: - raise PactTestException('Missing "tear_down" method in "pact_helper.py".') + raise PactTestException(MISSING_TEAR_DOWN) def path_to_pact_helper(self): path = os.path.join(self.config.consumer_tests_path, 'pact_helper.py') if os.path.isfile(path) is False: - msg = 'Missing "pact_helper.py" at "' + self.config.consumer_tests_path + '".' + msg = MISSING_PACT_HELPER + self.config.consumer_tests_path + '".' raise PactTestException(msg) return path diff --git a/pact_test/utils/__init__.py b/pact_test/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pact_test/utils/pact_utils.py b/pact_test/utils/pact_utils.py new file mode 100644 index 0000000..ced621e --- /dev/null +++ b/pact_test/utils/pact_utils.py @@ -0,0 +1,24 @@ +import json +try: # pragma: no cover + from urllib.request import urlopen # pragma: no cover +except ImportError: # pragma: no cover + from urllib import urlopen # pragma: no cover + + +def get_pact(location): + if location.startswith(('http', 'https')): + return __get_pact_from_url(location) + return __get_pact_from_file(location) + + +def __get_pact_from_file(filename): + with open(filename) as file_content: + return json.loads(file_content.read()) + + +def __get_pact_from_url(url): + return json.loads(__url_content(url)) + + +def __url_content(url): # pragma: no cover + return urlopen(url).read() # pragma: no cover diff --git a/tests/either/test_either.py b/tests/either/test_either.py new file mode 100644 index 0000000..3d231dd --- /dev/null +++ b/tests/either/test_either.py @@ -0,0 +1,47 @@ +from pact_test.either import * + + +def test_left_value(): + l = Left(42) + assert l.value == 42 + + +def test_right_value(): + r = Right(42) + assert r.value == 42 + + +def test_concat_right(): + out = minus_one(45).concat(minus_one).concat(minus_one) + assert out.value == 42 + + +def test_concat_left(): + out = minus_one(1).concat(one_divided_by) + assert out.value == "Division by zero." + + +def test_break_the_chain(): + out = minus_one(1).concat(one_divided_by).concat(minus_one) + assert out.value == "Division by zero." + + +def test_multiple_parameters(): + out = minus_one(9).concat(my_sum, 5) + assert out.value == 13 + + +def minus_one(value): + return Right(value - 1) + + +def one_divided_by(value): + try: + result = Right(1 / value) + except ZeroDivisionError: + result = Left("Division by zero.") + return result + + +def my_sum(a, b): + return Right(a + b) diff --git a/tests/models/test_service_consumer_test.py b/tests/models/test_service_consumer_test.py index d51b181..9740311 100644 --- a/tests/models/test_service_consumer_test.py +++ b/tests/models/test_service_consumer_test.py @@ -1,3 +1,4 @@ +import os import pytest from pact_test.exceptions import PactTestException from pact_test.models.service_consumer_test import state @@ -67,8 +68,8 @@ def setup(self): return 42 with pytest.raises(PactTestException) as e: - MyTest().is_valid() - assert str(e.value) == 'Missing setup for "pact_uri".' + MyTest().validate() + assert str(e.value).startswith('Missing setup for "pact_uri"') def test_missing_has_pact_with(): @@ -79,5 +80,5 @@ def setup(self): return 42 with pytest.raises(PactTestException) as e: - MyTest().is_valid() - assert str(e.value) == 'Missing setup for "has_pact_with".' + MyTest().validate() + assert str(e.value).startswith('Missing setup for "has_pact_with"') diff --git a/tests/resources/invalid_service_consumer/customer.py b/tests/resources/invalid_service_consumer/customer.py new file mode 100644 index 0000000..cbc48f8 --- /dev/null +++ b/tests/resources/invalid_service_consumer/customer.py @@ -0,0 +1,9 @@ +from pact_test.models.service_consumer_test import * + + +@pact_uri('http://google.com/') +class TestRestaurantCustomer(ServiceConsumerTest): + + @state('the breakfast is available') + def test_get_breakfast(self): + return 'Spam & Eggs' diff --git a/tests/resources/pact_files/file.json b/tests/resources/pact_files/file.json new file mode 100644 index 0000000..f0620a2 --- /dev/null +++ b/tests/resources/pact_files/file.json @@ -0,0 +1,3 @@ +{ + "spam": "eggs" +} \ No newline at end of file diff --git a/tests/resources/pact_files/simple.json b/tests/resources/pact_files/simple.json new file mode 100644 index 0000000..dc49e93 --- /dev/null +++ b/tests/resources/pact_files/simple.json @@ -0,0 +1,34 @@ +{ + "provider": { + "name": "Restaurant" + }, + "consumer": { + "name": "Customer" + }, + "interactions": [ + { + "providerState" : "the breakfast is available", + "description": "a request to get a breakfast item", + "request": { + "method": "GET", + "path": "/breakfast/42", + "query": "" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application-json/html" + }, + "body": { + "id": 42, + "name": "Spam & Eggs" + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "1.0.0" + } + } +} \ No newline at end of file diff --git a/tests/runners/test_consumer_tests_runner.py b/tests/runners/test_consumer_tests_runner.py index 4647d2e..c3376fb 100644 --- a/tests/runners/test_consumer_tests_runner.py +++ b/tests/runners/test_consumer_tests_runner.py @@ -1,9 +1,11 @@ import os import sys +import imp +import json import pytest -from pact_test.runners.consumer_tests_runner import ConsumerTestsRunner from pact_test.config.config_builder import Config from pact_test.exceptions import PactTestException +from pact_test.runners.consumer_tests_runner import ConsumerTestsRunner def test_missing_pact_helper(): @@ -78,6 +80,56 @@ def test_collect_tests(): assert state() == 'Spam & Eggs' +def test_invalid_test(): + path = os.path.join(os.getcwd(), 'tests', 'resources', + 'invalid_service_consumer', 'customer.py') + module = imp.load_source('invalid_test', path) + test = module.TestRestaurantCustomer + + t = ConsumerTestsRunner(None) + + with pytest.raises(PactTestException) as e: + t.verify_test(test) + assert str(e.value).startswith('Missing setup for "has_pact_with"') + + +def test_verify_missing_state(mocker): + def pact_content(_): + s = '{"interactions": [{"providerState": "My State"}]}' + return json.loads(s) + + path = os.path.join(os.getcwd(), 'tests', 'resources', + 'service_consumers', 'test_restaurant_customer.py') + module = imp.load_source('consumer_test', path) + test = module.TestRestaurantCustomer + + t = ConsumerTestsRunner(None) + mocker.patch.object(t, 'get_pact', new=pact_content) + + with pytest.raises(PactTestException) as e: + t.verify_test(test) + assert str(e.value) == 'Missing implementation for state "My State".' + + +def test_verify_existing_state(mocker): + def pact_content(_): + s = '{"interactions": [{"providerState": ' \ + '"the breakfast is available"}]}' + return json.loads(s) + + path = os.path.join(os.getcwd(), 'tests', 'resources', + 'service_consumers', 'test_restaurant_customer.py') + module = imp.load_source('consumer_test', path) + test = module.TestRestaurantCustomer + + t = ConsumerTestsRunner(None) + mocker.patch.object(t, 'get_pact', new=pact_content) + mocker.spy(t, 'verify_state') + + t.verify_test(test) + assert t.verify_state.call_count == 1 + + def remove_pact_helper(): try: del sys.modules['pact_helper'] diff --git a/tests/utils/test_pact_utils.py b/tests/utils/test_pact_utils.py new file mode 100644 index 0000000..249e074 --- /dev/null +++ b/tests/utils/test_pact_utils.py @@ -0,0 +1,44 @@ +import os +try: + from urllib.request import urlopen +except ImportError: + from urllib import urlopen +from pact_test.utils import pact_utils + + +def test_get_pact_from_url(monkeypatch): + def fake_website(_): + return '{"spam": "eggs"}' + monkeypatch.setattr(pact_utils, '__url_content', fake_website) + + url = 'http://montyphyton.com/' + url_content = {'spam': 'eggs'} + + assert pact_utils.__get_pact_from_url(url) == url_content + + +def test_get_pact_from_file(): + filename = os.path.join(os.getcwd(), 'tests', 'resources', + 'pact_files', 'file.json') + file_content = {'spam': 'eggs'} + + assert pact_utils.__get_pact_from_file(filename) == file_content + + +def test_generic_get_pact_from_url(monkeypatch): + def fake_website(_): + return '{"spam": "eggs"}' + monkeypatch.setattr(pact_utils, '__url_content', fake_website) + + url = 'http://montyphyton.com/' + url_content = {'spam': 'eggs'} + + assert pact_utils.get_pact(url) == url_content + + +def test_generic_get_pact_from_file(): + filename = os.path.join(os.getcwd(), 'tests', 'resources', + 'pact_files', 'file.json') + file_content = {'spam': 'eggs'} + + assert pact_utils.get_pact(filename) == file_content From a6a224f06d6785fae2573055756755bcec6ac318 Mon Sep 17 00:00:00 2001 From: Guido Barbaglia Date: Wed, 31 May 2017 13:22:36 +1000 Subject: [PATCH 15/85] Better syntax for Eithers Either class overrides `__rshift__` magic method to allow the usage of `>>` to cancatenate several functions. Example: ```python def test_break_the_chain(): out = minus_one(1) >> one_divided_by >> minus_one assert out.value == "Division by zero." ``` The `concat` function remains to allow the concatenation of functions that require more than one input parameter. Example: ```python def test_multiple_parameters(): out = minus_one(9).concat(my_sum, 5) assert out.value == 13 def my_sum(a, b): return Right(a + b) ``` --- pact_test/either/__init__.py | 6 ++++++ tests/either/test_either.py | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pact_test/either/__init__.py b/pact_test/either/__init__.py index 0764ac8..b82d132 100644 --- a/pact_test/either/__init__.py +++ b/pact_test/either/__init__.py @@ -5,6 +5,12 @@ class Either(object): def __init__(self, value): self.value = value + def __rshift__(self, f): + if type(self) is Right: + return f(self.value) + else: + return self + def concat(self, f, *args): if type(self) is Right: return f(self.value, *args) diff --git a/tests/either/test_either.py b/tests/either/test_either.py index 3d231dd..a87728c 100644 --- a/tests/either/test_either.py +++ b/tests/either/test_either.py @@ -12,17 +12,17 @@ def test_right_value(): def test_concat_right(): - out = minus_one(45).concat(minus_one).concat(minus_one) + out = minus_one(45) >> minus_one >> minus_one assert out.value == 42 def test_concat_left(): - out = minus_one(1).concat(one_divided_by) + out = minus_one(1) >> one_divided_by assert out.value == "Division by zero." def test_break_the_chain(): - out = minus_one(1).concat(one_divided_by).concat(minus_one) + out = minus_one(1) >> one_divided_by >> minus_one assert out.value == "Division by zero." From fad2aa1223b748eb0c2d9aa4c6b29956d2639dfb Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Wed, 31 May 2017 19:03:25 +1000 Subject: [PATCH 16/85] WIP: refactoring with Either. --- pact_test/exceptions/__init__.py | 2 -- pact_test/models/service_consumer_test.py | 9 ++--- pact_test/runners/consumer_tests_runner.py | 37 +++++++++++---------- tests/exceptions/test_pact_exception.py | 8 ----- tests/models/test_service_consumer_test.py | 13 +++----- tests/runners/test_consumer_tests_runner.py | 31 +++++------------ 6 files changed, 38 insertions(+), 62 deletions(-) delete mode 100644 pact_test/exceptions/__init__.py delete mode 100644 tests/exceptions/test_pact_exception.py diff --git a/pact_test/exceptions/__init__.py b/pact_test/exceptions/__init__.py deleted file mode 100644 index 175625d..0000000 --- a/pact_test/exceptions/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -class PactTestException(Exception): - pass diff --git a/pact_test/models/service_consumer_test.py b/pact_test/models/service_consumer_test.py index 05b114f..6fed724 100644 --- a/pact_test/models/service_consumer_test.py +++ b/pact_test/models/service_consumer_test.py @@ -1,5 +1,5 @@ +from pact_test.either import * from pact_test.constants import * -from pact_test.exceptions import PactTestException class ServiceConsumerTest(object): @@ -13,13 +13,14 @@ def states(self): if callable(obj) and hasattr(obj, 'state'): yield obj - def validate(self): + def is_valid(self): if self.pact_uri is None: msg = MISSING_PACT_URI + __file__ - raise PactTestException(msg) + return Left(msg) if self.has_pact_with is None: msg = MISSING_HAS_PACT_WITH + __file__ - raise PactTestException(msg) + return Left(msg) + return Right(True) def pact_uri(pact_uri_value): diff --git a/pact_test/runners/consumer_tests_runner.py b/pact_test/runners/consumer_tests_runner.py index 0e5a423..ebc205a 100644 --- a/pact_test/runners/consumer_tests_runner.py +++ b/pact_test/runners/consumer_tests_runner.py @@ -1,8 +1,8 @@ import os import imp import inspect +from pact_test.either import * from pact_test.constants import * -from pact_test.exceptions import PactTestException from pact_test.utils.pact_utils import get_pact @@ -17,17 +17,20 @@ def verify(self): def verify_test(self, test_class): test = test_class() - test.validate() - pact = self.get_pact(test.pact_uri) + validity_check = test.is_valid() + if type(validity_check) is Right: + pact = self.get_pact(test.pact_uri) - pact_states = list(map(lambda i: i['providerState'], pact['interactions'])) - test_states = list(map(lambda s: s.state, test.states)) + pact_states = list(map(lambda i: i['providerState'], pact['interactions'])) + test_states = list(map(lambda s: s.state, test.states)) - for pact_state in pact_states: - if pact_state not in test_states: - msg = MISSING_STATE + '"' + pact_state + '".' - raise PactTestException(msg) - self.verify_state(test.states, pact_state) + for pact_state in pact_states: + if pact_state not in test_states: + msg = MISSING_STATE + '"' + pact_state + '".' + return Left(msg) + self.verify_state(test.states, pact_state) + else: + return validity_check def verify_state(self, states, provider_state): # for s in states: @@ -59,25 +62,25 @@ def collect_tests(self): tests.append(obj) if not files: - raise PactTestException(MISSING_TESTS) - return tests + return Left(MISSING_TESTS) + return Right(tests) def all_files(self): return os.listdir(self.config.consumer_tests_path) def load_pact_helper(self): - self.pact_helper = imp.load_source('pact_helper', self.path_to_pact_helper()) + self.pact_helper = imp.load_source('pact_helper', self.path_to_pact_helper().value) if hasattr(self.pact_helper, 'setup') is False: - raise PactTestException(MISSING_SETUP) + return Left(MISSING_SETUP) if hasattr(self.pact_helper, 'tear_down') is False: - raise PactTestException(MISSING_TEAR_DOWN) + return Left(MISSING_TEAR_DOWN) def path_to_pact_helper(self): path = os.path.join(self.config.consumer_tests_path, 'pact_helper.py') if os.path.isfile(path) is False: msg = MISSING_PACT_HELPER + self.config.consumer_tests_path + '".' - raise PactTestException(msg) - return path + return Left(msg) + return Right(path) def filter_rule(filename): diff --git a/tests/exceptions/test_pact_exception.py b/tests/exceptions/test_pact_exception.py deleted file mode 100644 index d6ea46d..0000000 --- a/tests/exceptions/test_pact_exception.py +++ /dev/null @@ -1,8 +0,0 @@ -from pact_test.exceptions import PactTestException - - -def test_message(): - try: - raise PactTestException('Pact Error') - except PactTestException as e: - assert str(e) == 'Pact Error' diff --git a/tests/models/test_service_consumer_test.py b/tests/models/test_service_consumer_test.py index 9740311..0dcc06b 100644 --- a/tests/models/test_service_consumer_test.py +++ b/tests/models/test_service_consumer_test.py @@ -1,6 +1,3 @@ -import os -import pytest -from pact_test.exceptions import PactTestException from pact_test.models.service_consumer_test import state from pact_test.models.service_consumer_test import pact_uri from pact_test.models.service_consumer_test import has_pact_with @@ -67,9 +64,8 @@ class MyTest(ServiceConsumerTest): def setup(self): return 42 - with pytest.raises(PactTestException) as e: - MyTest().validate() - assert str(e.value).startswith('Missing setup for "pact_uri"') + msg = 'Missing setup for "pact_uri"' + assert MyTest().is_valid().value.startswith(msg) def test_missing_has_pact_with(): @@ -79,6 +75,5 @@ class MyTest(ServiceConsumerTest): def setup(self): return 42 - with pytest.raises(PactTestException) as e: - MyTest().validate() - assert str(e.value).startswith('Missing setup for "has_pact_with"') + msg = 'Missing setup for "has_pact_with"' + assert MyTest().is_valid().value.startswith(msg) diff --git a/tests/runners/test_consumer_tests_runner.py b/tests/runners/test_consumer_tests_runner.py index c3376fb..c645283 100644 --- a/tests/runners/test_consumer_tests_runner.py +++ b/tests/runners/test_consumer_tests_runner.py @@ -2,9 +2,7 @@ import sys import imp import json -import pytest from pact_test.config.config_builder import Config -from pact_test.exceptions import PactTestException from pact_test.runners.consumer_tests_runner import ConsumerTestsRunner @@ -12,9 +10,7 @@ def test_missing_pact_helper(): config = Config() t = ConsumerTestsRunner(config) msg = 'Missing "pact_helper.py" at "tests/service_consumers".' - with pytest.raises(PactTestException) as e: - t.path_to_pact_helper() - assert str(e.value) == msg + assert t.path_to_pact_helper().value == msg def test_missing_setup_method(): @@ -26,9 +22,7 @@ def test_missing_setup_method(): config.consumer_tests_path = test_pact_helper_path t = ConsumerTestsRunner(config) msg = 'Missing "setup" method in "pact_helper.py".' - with pytest.raises(PactTestException) as e: - t.load_pact_helper() - assert str(e.value) == msg + assert t.load_pact_helper().value == msg def test_missing_tear_down_method(): @@ -40,9 +34,7 @@ def test_missing_tear_down_method(): config.consumer_tests_path = test_pact_helper_path t = ConsumerTestsRunner(config) msg = 'Missing "tear_down" method in "pact_helper.py".' - with pytest.raises(PactTestException) as e: - t.load_pact_helper() - assert str(e.value) == msg + assert t.load_pact_helper().value == msg def test_empty_tests_list(monkeypatch): @@ -56,9 +48,7 @@ def empty_list(_): monkeypatch.setattr(os, 'listdir', empty_list) t = ConsumerTestsRunner(config) - with pytest.raises(PactTestException) as e: - t.collect_tests() - assert str(e.value) == 'There are no consumer tests to verify.' + assert t.collect_tests().value == 'There are no consumer tests to verify.' def test_collect_tests(): @@ -68,7 +58,7 @@ def test_collect_tests(): config.consumer_tests_path = test_pact_helper_path t = ConsumerTestsRunner(config) - tests = t.collect_tests() + tests = t.collect_tests().value assert len(tests) == 1 test = tests[0]() @@ -87,10 +77,8 @@ def test_invalid_test(): test = module.TestRestaurantCustomer t = ConsumerTestsRunner(None) - - with pytest.raises(PactTestException) as e: - t.verify_test(test) - assert str(e.value).startswith('Missing setup for "has_pact_with"') + msg = 'Missing setup for "has_pact_with"' + assert t.verify_test(test).value.startswith(msg) def test_verify_missing_state(mocker): @@ -106,9 +94,8 @@ def pact_content(_): t = ConsumerTestsRunner(None) mocker.patch.object(t, 'get_pact', new=pact_content) - with pytest.raises(PactTestException) as e: - t.verify_test(test) - assert str(e.value) == 'Missing implementation for state "My State".' + msg = 'Missing implementation for state "My State".' + assert t.verify_test(test).value == msg def test_verify_existing_state(mocker): From 508085cfd1e935f40cf506f54350702fd51edacc Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 3 Jun 2017 15:47:13 +1000 Subject: [PATCH 17/85] Classifiers --- setup.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 42546b6..783dc4a 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.1.1', + version='0.1.2', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), @@ -12,7 +12,22 @@ description='Python implementation for Pact (http://pact.io/)', install_requires=[], setup_requires=['pytest-runner'], - tests_require=['pytest>=3.0', 'pytest-pep8', 'pytest-sugar', 'pytest-mock'], + tests_require=[ + 'pytest>=3.0', + 'pytest-pep8', + 'pytest-sugar', + 'pytest-mock' + ], url='https://github.com/Kalimaha/pact-test/', - scripts=['bin/pact-test'] + scripts=['bin/pact-test'], + classifiers=[ + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Topic :: Software Development :: Testing' + ] ) From d13363535175588357f21599ac46e63e5a114e6f Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 3 Jun 2017 15:47:13 +1000 Subject: [PATCH 18/85] Classifiers --- pact_test/runners/pact_tests_runner.py | 6 +- .../runners/service_consumers/__init__.py | 0 .../runners/service_consumers/state_test.py | 25 ++++++ .../test_suite.py} | 5 +- .../runners/service_providers/__init__.py | 0 .../provider_tests_runner.py | 0 setup.py | 21 ++++- ...st_config_builder.py => config_builder.py} | 0 tests/either/{test_either.py => either.py} | 5 ++ ...sumer_test.py => service_consumer_test.py} | 0 ...vider_test.py => service_provider_test.py} | 0 ...t_tests_runner.py => pact_tests_runner.py} | 0 tests/runners/service_consumers/__init__.py | 0 tests/runners/service_consumers/state_test.py | 77 +++++++++++++++++++ .../test_suite.py} | 24 +++--- .../{test_pact_utils.py => pact_utils.py} | 0 16 files changed, 142 insertions(+), 21 deletions(-) create mode 100644 pact_test/runners/service_consumers/__init__.py create mode 100644 pact_test/runners/service_consumers/state_test.py rename pact_test/runners/{consumer_tests_runner.py => service_consumers/test_suite.py} (96%) create mode 100644 pact_test/runners/service_providers/__init__.py rename pact_test/runners/{ => service_providers}/provider_tests_runner.py (100%) rename tests/config/{test_config_builder.py => config_builder.py} (100%) rename tests/either/{test_either.py => either.py} (87%) rename tests/models/{test_service_consumer_test.py => service_consumer_test.py} (100%) rename tests/models/{test_service_provider_test.py => service_provider_test.py} (100%) rename tests/runners/{test_pact_tests_runner.py => pact_tests_runner.py} (100%) create mode 100644 tests/runners/service_consumers/__init__.py create mode 100644 tests/runners/service_consumers/state_test.py rename tests/runners/{test_consumer_tests_runner.py => service_consumers/test_suite.py} (85%) rename tests/utils/{test_pact_utils.py => pact_utils.py} (100%) diff --git a/pact_test/runners/pact_tests_runner.py b/pact_test/runners/pact_tests_runner.py index ea279e4..b9fc6e8 100644 --- a/pact_test/runners/pact_tests_runner.py +++ b/pact_test/runners/pact_tests_runner.py @@ -1,6 +1,6 @@ from pact_test.config.config_builder import Config -from pact_test.runners.consumer_tests_runner import ConsumerTestsRunner -from pact_test.runners.provider_tests_runner import ProviderTestsRunner +from pact_test.runners.service_consumers.test_suite import ServiceConsumerTestSuiteRunner +from pact_test.runners.service_providers.provider_tests_runner import ProviderTestsRunner def verify(verify_consumers=False, verify_providers=False): @@ -12,7 +12,7 @@ def verify(verify_consumers=False, verify_providers=False): def run_consumer_tests(config): - ConsumerTestsRunner(config).verify() + ServiceConsumerTestSuiteRunner(config).verify() def run_provider_tests(config): diff --git a/pact_test/runners/service_consumers/__init__.py b/pact_test/runners/service_consumers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pact_test/runners/service_consumers/state_test.py b/pact_test/runners/service_consumers/state_test.py new file mode 100644 index 0000000..48a3e65 --- /dev/null +++ b/pact_test/runners/service_consumers/state_test.py @@ -0,0 +1,25 @@ +from pact_test.either import * +try: + from urllib.request import Request, urlopen +except: + from urllib2 import Request, urlopen + + +def verify_state(interaction, pact_helper, test_instance): + pact_helper.set_up() + pact_helper.tear_down() + return Right(build_test_result()) + + +def create_request(request_body): + request = Request('http://demo7688835.mockable.io') + request.method = request_body['method'] + request.selector = request_body['path'] + request_body['query'] + request_headers = request_body.get('headers', {}) + for header in request_headers: + request.add_header(header, request_headers[header]) + return request + + +def build_test_result(status='PASSED', reason=None): + return {'status': status, 'reason': reason} diff --git a/pact_test/runners/consumer_tests_runner.py b/pact_test/runners/service_consumers/test_suite.py similarity index 96% rename from pact_test/runners/consumer_tests_runner.py rename to pact_test/runners/service_consumers/test_suite.py index ebc205a..70f348a 100644 --- a/pact_test/runners/consumer_tests_runner.py +++ b/pact_test/runners/service_consumers/test_suite.py @@ -6,7 +6,7 @@ from pact_test.utils.pact_utils import get_pact -class ConsumerTestsRunner(object): +class ServiceConsumerTestSuiteRunner(object): pact_helper = None def __init__(self, config): @@ -15,8 +15,7 @@ def __init__(self, config): def verify(self): pass - def verify_test(self, test_class): - test = test_class() + def verify_test(self, test): validity_check = test.is_valid() if type(validity_check) is Right: pact = self.get_pact(test.pact_uri) diff --git a/pact_test/runners/service_providers/__init__.py b/pact_test/runners/service_providers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pact_test/runners/provider_tests_runner.py b/pact_test/runners/service_providers/provider_tests_runner.py similarity index 100% rename from pact_test/runners/provider_tests_runner.py rename to pact_test/runners/service_providers/provider_tests_runner.py diff --git a/setup.py b/setup.py index 42546b6..783dc4a 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.1.1', + version='0.1.2', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), @@ -12,7 +12,22 @@ description='Python implementation for Pact (http://pact.io/)', install_requires=[], setup_requires=['pytest-runner'], - tests_require=['pytest>=3.0', 'pytest-pep8', 'pytest-sugar', 'pytest-mock'], + tests_require=[ + 'pytest>=3.0', + 'pytest-pep8', + 'pytest-sugar', + 'pytest-mock' + ], url='https://github.com/Kalimaha/pact-test/', - scripts=['bin/pact-test'] + scripts=['bin/pact-test'], + classifiers=[ + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Topic :: Software Development :: Testing' + ] ) diff --git a/tests/config/test_config_builder.py b/tests/config/config_builder.py similarity index 100% rename from tests/config/test_config_builder.py rename to tests/config/config_builder.py diff --git a/tests/either/test_either.py b/tests/either/either.py similarity index 87% rename from tests/either/test_either.py rename to tests/either/either.py index a87728c..6e4e270 100644 --- a/tests/either/test_either.py +++ b/tests/either/either.py @@ -31,6 +31,11 @@ def test_multiple_parameters(): assert out.value == 13 +def test_multiple_parameters_left(): + out = one_divided_by(0).concat(my_sum, 5) + assert out.value == "Division by zero." + + def minus_one(value): return Right(value - 1) diff --git a/tests/models/test_service_consumer_test.py b/tests/models/service_consumer_test.py similarity index 100% rename from tests/models/test_service_consumer_test.py rename to tests/models/service_consumer_test.py diff --git a/tests/models/test_service_provider_test.py b/tests/models/service_provider_test.py similarity index 100% rename from tests/models/test_service_provider_test.py rename to tests/models/service_provider_test.py diff --git a/tests/runners/test_pact_tests_runner.py b/tests/runners/pact_tests_runner.py similarity index 100% rename from tests/runners/test_pact_tests_runner.py rename to tests/runners/pact_tests_runner.py diff --git a/tests/runners/service_consumers/__init__.py b/tests/runners/service_consumers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/runners/service_consumers/state_test.py b/tests/runners/service_consumers/state_test.py new file mode 100644 index 0000000..f5e7637 --- /dev/null +++ b/tests/runners/service_consumers/state_test.py @@ -0,0 +1,77 @@ +from pact_test.models.service_consumer_test import state +from pact_test.models.service_consumer_test import ServiceConsumerTest +from pact_test.runners.service_consumers.state_test import verify_state +from pact_test.runners.service_consumers.state_test import create_request +from pact_test.runners.service_consumers.state_test import build_test_result + + +def test_build_test_result(): + status = 'Spam' + reason = 'Eggs' + expected_response = {'status': status, 'reason': reason} + assert build_test_result(status, reason) == expected_response + + +def test_build_test_result_default(): + expected_response = {'status': 'PASSED', 'reason': None} + assert build_test_result() == expected_response + + +def test_verify_state(mocker): + test_instance = TestLibraryApp() + pact_helper = PactHelper() + + mocker.spy(pact_helper, 'set_up') + mocker.spy(pact_helper, 'tear_down') + + response = verify_state(interaction, pact_helper, test_instance).value + expected_response = {'status': 'PASSED', 'reason': None} + + assert response == expected_response + assert pact_helper.set_up.call_count == 1 + assert pact_helper.tear_down.call_count == 1 + + +def test_create_request(): + request_body = interaction['request'] + request = create_request(request_body) + + assert request.method == request_body['method'] + assert request.selector == request_body['path'] + request_body['query'] + assert request.headers == request_body.get('headers', {}) + + +class PactHelper(object): + @staticmethod + def set_up(): + pass + + @staticmethod + def tear_down(): + pass + + +interaction = { + 'providerState': 'some books exist', + 'request': { + 'method': 'GET', + 'path': '/books/42', + 'query': '?type=hardcover', + 'headers': { + 'Content-type': 'application/json' + } + }, + 'response': { + 'status': 200, + 'body': { + 'id': 42, + 'title': 'The Hitchhicker\'s Guide to the Galaxy' + } + } +} + + +class TestLibraryApp(ServiceConsumerTest): + @state('some books exist') + def test_get_book(self): + pass diff --git a/tests/runners/test_consumer_tests_runner.py b/tests/runners/service_consumers/test_suite.py similarity index 85% rename from tests/runners/test_consumer_tests_runner.py rename to tests/runners/service_consumers/test_suite.py index c645283..2867b80 100644 --- a/tests/runners/test_consumer_tests_runner.py +++ b/tests/runners/service_consumers/test_suite.py @@ -3,12 +3,12 @@ import imp import json from pact_test.config.config_builder import Config -from pact_test.runners.consumer_tests_runner import ConsumerTestsRunner +from pact_test.runners.service_consumers.test_suite import ServiceConsumerTestSuiteRunner # nopep8 def test_missing_pact_helper(): config = Config() - t = ConsumerTestsRunner(config) + t = ServiceConsumerTestSuiteRunner(config) msg = 'Missing "pact_helper.py" at "tests/service_consumers".' assert t.path_to_pact_helper().value == msg @@ -20,7 +20,7 @@ def test_missing_setup_method(): test_pact_helper_path = os.path.join(os.getcwd(), 'tests', 'resources', 'pact_helper_no_setup') config.consumer_tests_path = test_pact_helper_path - t = ConsumerTestsRunner(config) + t = ServiceConsumerTestSuiteRunner(config) msg = 'Missing "setup" method in "pact_helper.py".' assert t.load_pact_helper().value == msg @@ -32,7 +32,7 @@ def test_missing_tear_down_method(): test_pact_helper_path = os.path.join(os.getcwd(), 'tests', 'resources', 'pact_helper_no_tear_down') config.consumer_tests_path = test_pact_helper_path - t = ConsumerTestsRunner(config) + t = ServiceConsumerTestSuiteRunner(config) msg = 'Missing "tear_down" method in "pact_helper.py".' assert t.load_pact_helper().value == msg @@ -47,7 +47,7 @@ def empty_list(_): return [] monkeypatch.setattr(os, 'listdir', empty_list) - t = ConsumerTestsRunner(config) + t = ServiceConsumerTestSuiteRunner(config) assert t.collect_tests().value == 'There are no consumer tests to verify.' @@ -56,7 +56,7 @@ def test_collect_tests(): test_pact_helper_path = os.path.join(os.getcwd(), 'tests', 'resources', 'service_consumers') config.consumer_tests_path = test_pact_helper_path - t = ConsumerTestsRunner(config) + t = ServiceConsumerTestSuiteRunner(config) tests = t.collect_tests().value assert len(tests) == 1 @@ -74,9 +74,9 @@ def test_invalid_test(): path = os.path.join(os.getcwd(), 'tests', 'resources', 'invalid_service_consumer', 'customer.py') module = imp.load_source('invalid_test', path) - test = module.TestRestaurantCustomer + test = module.TestRestaurantCustomer() - t = ConsumerTestsRunner(None) + t = ServiceConsumerTestSuiteRunner(None) msg = 'Missing setup for "has_pact_with"' assert t.verify_test(test).value.startswith(msg) @@ -89,9 +89,9 @@ def pact_content(_): path = os.path.join(os.getcwd(), 'tests', 'resources', 'service_consumers', 'test_restaurant_customer.py') module = imp.load_source('consumer_test', path) - test = module.TestRestaurantCustomer + test = module.TestRestaurantCustomer() - t = ConsumerTestsRunner(None) + t = ServiceConsumerTestSuiteRunner(None) mocker.patch.object(t, 'get_pact', new=pact_content) msg = 'Missing implementation for state "My State".' @@ -107,9 +107,9 @@ def pact_content(_): path = os.path.join(os.getcwd(), 'tests', 'resources', 'service_consumers', 'test_restaurant_customer.py') module = imp.load_source('consumer_test', path) - test = module.TestRestaurantCustomer + test = module.TestRestaurantCustomer() - t = ConsumerTestsRunner(None) + t = ServiceConsumerTestSuiteRunner(None) mocker.patch.object(t, 'get_pact', new=pact_content) mocker.spy(t, 'verify_state') diff --git a/tests/utils/test_pact_utils.py b/tests/utils/pact_utils.py similarity index 100% rename from tests/utils/test_pact_utils.py rename to tests/utils/pact_utils.py From 313843994d358e7021ac70b4a2aa58fde755ead9 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 10 Jun 2017 10:30:39 +1000 Subject: [PATCH 19/85] Convenience imports in main module. --- pact_test/__init__.py | 24 ++ pact_test/matchers/__init__.py | 0 pact_test/matchers/request_matcher.py | 58 ++++ pact_test/models/pact_helper.py | 7 + pact_test/runners/pact_tests_runner.py | 1 + .../runners/service_consumers/state_test.py | 50 +++- .../runners/service_consumers/test_suite.py | 24 +- pact_test/utils/pact_helper_utils.py | 42 +++ requirements.txt | 5 + tests/matchers/request_matcher.py | 90 +++++++ tests/models/pact_helper.py | 12 + tests/models/service_consumer_test.py | 8 +- tests/models/service_provider_test.py | 14 +- tests/resources/pact_helper/pact_helper.py | 11 +- .../pact_helper_no_setup/pact_helper.py | 8 +- .../pact_helper_no_tear_down/pact_helper.py | 8 +- tests/runners/service_consumers/state_test.py | 63 +++-- tests/runners/service_consumers/test_suite.py | 249 +++++++++--------- tests/utils/pact_helper_utils.py | 53 ++++ 19 files changed, 534 insertions(+), 193 deletions(-) create mode 100644 pact_test/matchers/__init__.py create mode 100644 pact_test/matchers/request_matcher.py create mode 100644 pact_test/models/pact_helper.py create mode 100644 pact_test/utils/pact_helper_utils.py create mode 100644 requirements.txt create mode 100644 tests/matchers/request_matcher.py create mode 100644 tests/models/pact_helper.py create mode 100644 tests/utils/pact_helper_utils.py diff --git a/pact_test/__init__.py b/pact_test/__init__.py index e69de29..b83849f 100644 --- a/pact_test/__init__.py +++ b/pact_test/__init__.py @@ -0,0 +1,24 @@ +from pact_test.models.pact_helper import PactHelper +from pact_test.models.service_consumer_test import state +from pact_test.models.service_consumer_test import pact_uri +from pact_test.models.service_provider_test import given +from pact_test.models.service_provider_test import with_request +from pact_test.models.service_provider_test import has_pact_with +from pact_test.models.service_provider_test import upon_receiving +from pact_test.models.service_provider_test import service_consumer +from pact_test.models.service_provider_test import will_respond_with +from pact_test.models.service_consumer_test import ServiceConsumerTest +from pact_test.models.service_provider_test import ServiceProviderTest + + +state = state +given = given +pact_uri = pact_uri +PactHelper = PactHelper +with_request = with_request +has_pact_with = has_pact_with +upon_receiving = upon_receiving +service_consumer = service_consumer +will_respond_with = will_respond_with +ServiceConsumerTest = ServiceConsumerTest +ServiceProviderTest = ServiceProviderTest diff --git a/pact_test/matchers/__init__.py b/pact_test/matchers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pact_test/matchers/request_matcher.py b/pact_test/matchers/request_matcher.py new file mode 100644 index 0000000..f8d3fbd --- /dev/null +++ b/pact_test/matchers/request_matcher.py @@ -0,0 +1,58 @@ +from pact_test.either import * + + +def match(expected, actual): + body = actual.data + method = actual.method + headers = actual.headers + path = actual.selector[0:actual.selector.index('?')] + query = actual.selector[actual.selector.index('?'):] + + return _match_query(expected, query)\ + .concat(_match_path, path)\ + .concat(_match_method, method)\ + .concat(_match_body, body)\ + .concat(_match_headers, headers) + + +def _match_body(expected, actual): + matching_texts = actual == expected['body'] + same_type = type(expected['body']) == type(actual) + matching_dicts = type(actual) is dict and _is_subset(actual, expected['body']) + + if same_type and (matching_dicts or matching_texts): + return Right(expected) + return Left(_build_error_message('body', expected['body'], actual)) + + +def _match_query(expected, actual): + if actual == expected['query']: + return Right(expected) + return Left(_build_error_message('query', expected['query'], actual)) + + +def _match_path(expected, actual): + if actual == expected['path']: + return Right(expected) + return Left(_build_error_message('path', expected['path'], actual)) + + +def _match_method(expected, actual): + if actual == expected['method']: + return Right(expected) + return Left(_build_error_message('method', expected['method'], actual)) + + +def _match_headers(expected, actual): + if _is_subset(actual, expected['headers']): + return Right(expected) + return Left(_build_error_message('headers', expected['headers'], actual)) + + +def _build_error_message(section, expected, actual): + return 'Non-matching ' + section + ' for the request. Expected:\n\n\t' + \ + str(expected) + '\n\nReceived:\n\n\t' + str(actual) + + +def _is_subset(expected, actual): + return all(item in expected.items() for item in actual.items()) diff --git a/pact_test/models/pact_helper.py b/pact_test/models/pact_helper.py new file mode 100644 index 0000000..64f442b --- /dev/null +++ b/pact_test/models/pact_helper.py @@ -0,0 +1,7 @@ +MISSING_SETUP = 'Missing implementation of "setup" in PactHelper' +MISSING_TEAR_DOWN = 'Missing implementation of "tear_down" in PactHelper' + + +class PactHelper(object): + test_url = 'localhost' + test_port = 9999 diff --git a/pact_test/runners/pact_tests_runner.py b/pact_test/runners/pact_tests_runner.py index b9fc6e8..b457837 100644 --- a/pact_test/runners/pact_tests_runner.py +++ b/pact_test/runners/pact_tests_runner.py @@ -5,6 +5,7 @@ def verify(verify_consumers=False, verify_providers=False): config = Config() + if verify_consumers: run_consumer_tests(config) if verify_providers: diff --git a/pact_test/runners/service_consumers/state_test.py b/pact_test/runners/service_consumers/state_test.py index 48a3e65..1bf3f34 100644 --- a/pact_test/runners/service_consumers/state_test.py +++ b/pact_test/runners/service_consumers/state_test.py @@ -1,25 +1,55 @@ +import json from pact_test.either import * -try: - from urllib.request import Request, urlopen -except: - from urllib2 import Request, urlopen +try: # pragma: no cover + from urllib.request import Request, urlopen # pragma: no cover +except: # pragma: no cover + from urllib2 import Request, urlopen # pragma: no cover def verify_state(interaction, pact_helper, test_instance): - pact_helper.set_up() + pact_helper.setup() + request = _create_request(pact_helper.test_url, pact_helper.test_port, interaction['request']) + if type(request) is Right: + response = _parse_response(request.value) pact_helper.tear_down() - return Right(build_test_result()) + return Right(_build_test_result()) -def create_request(request_body): - request = Request('http://demo7688835.mockable.io') +def _verify_request(expected_request, request): + pass + + +def _create_request(url, port, request_body): + request_port = '80' if port is None else str(port) + request_url = 'http://' + url + ':' + request_port + + request = Request(request_url) request.method = request_body['method'] request.selector = request_body['path'] + request_body['query'] request_headers = request_body.get('headers', {}) for header in request_headers: request.add_header(header, request_headers[header]) - return request + return Right(request) + + +def _parse_response(request): + try: + response = urlopen(request) + response_body = response.read() + response_type = _get_response_type(response).value + if 'application/json' in response_type: + return Right(json.loads(response_body)) + return Right(str(response_body.decode('utf-8'))) + except Exception as e: + return Left(str(e)) + + +def _get_response_type(response): # pragma: no cover + try: # pragma: no cover + return Right(response.getheader('Content-type')) # pragma: no cover + except: # pragma: no cover + return Right(response.headers['Content-Type']) # pragma: no cover -def build_test_result(status='PASSED', reason=None): +def _build_test_result(status='PASSED', reason=None): return {'status': status, 'reason': reason} diff --git a/pact_test/runners/service_consumers/test_suite.py b/pact_test/runners/service_consumers/test_suite.py index 70f348a..d9b555c 100644 --- a/pact_test/runners/service_consumers/test_suite.py +++ b/pact_test/runners/service_consumers/test_suite.py @@ -2,6 +2,7 @@ import imp import inspect from pact_test.either import * +from pact_test.utils.pact_helper_utils import load_pact_helper from pact_test.constants import * from pact_test.utils.pact_utils import get_pact @@ -13,7 +14,14 @@ def __init__(self, config): self.config = config def verify(self): - pass + pact_helper = load_pact_helper(self.config.consumer_tests_path) + if type(pact_helper) is Right: + self.pact_helper = pact_helper + return self.collect_tests() >> self.verify_tests + return pact_helper + + def verify_tests(self, tests): + return list(map(self.verify_test, tests)) def verify_test(self, test): validity_check = test.is_valid() @@ -67,20 +75,6 @@ def collect_tests(self): def all_files(self): return os.listdir(self.config.consumer_tests_path) - def load_pact_helper(self): - self.pact_helper = imp.load_source('pact_helper', self.path_to_pact_helper().value) - if hasattr(self.pact_helper, 'setup') is False: - return Left(MISSING_SETUP) - if hasattr(self.pact_helper, 'tear_down') is False: - return Left(MISSING_TEAR_DOWN) - - def path_to_pact_helper(self): - path = os.path.join(self.config.consumer_tests_path, 'pact_helper.py') - if os.path.isfile(path) is False: - msg = MISSING_PACT_HELPER + self.config.consumer_tests_path + '".' - return Left(msg) - return Right(path) - def filter_rule(filename): return (filename != '__init__.py' and diff --git a/pact_test/utils/pact_helper_utils.py b/pact_test/utils/pact_helper_utils.py new file mode 100644 index 0000000..a60bc97 --- /dev/null +++ b/pact_test/utils/pact_helper_utils.py @@ -0,0 +1,42 @@ +import os +import imp +import inspect +from pact_test.either import * +from pact_test import PactHelper +from pact_test.constants import MISSING_SETUP +from pact_test.constants import MISSING_TEAR_DOWN +from pact_test.constants import MISSING_PACT_HELPER + + +def load_pact_helper(consumer_tests_path): + return _path_to_pact_helper(consumer_tests_path).concat(_load_module, 'pact_helper') >> _load_user_class + + +def _load_user_class(user_module): + user_class = None + + for name, obj in inspect.getmembers(user_module): + if inspect.isclass(obj) and len(inspect.getmro(obj)) > 2 and issubclass(obj, PactHelper): + user_class = obj() + + if hasattr(user_class, 'setup') is False: + return Left(MISSING_SETUP) + if hasattr(user_class, 'tear_down') is False: + return Left(MISSING_TEAR_DOWN) + + return Right(user_class) + + +def _load_module(path, module_name): + try: + return Right(imp.load_source(module_name, path)) + except Exception: + return Left(MISSING_PACT_HELPER + path + '".') + + +def _path_to_pact_helper(consumer_tests_path): + path = os.path.join(consumer_tests_path, 'pact_helper.py') + if os.path.isfile(path) is False: + msg = MISSING_PACT_HELPER + consumer_tests_path + '".' + return Left(msg) + return Right(path) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f650fda --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +pytest>=3.0 +pytest-pep8 +pytest-mock +pytest-sugar +pytest-runner diff --git a/tests/matchers/request_matcher.py b/tests/matchers/request_matcher.py new file mode 100644 index 0000000..0140a71 --- /dev/null +++ b/tests/matchers/request_matcher.py @@ -0,0 +1,90 @@ +from pact_test.either import * +from pact_test.matchers.request_matcher import match +try: # pragma: no cover + from urllib.request import Request, urlopen # pragma: no cover +except: # pragma: no cover + from urllib2 import Request, urlopen # pragma: no cover + + +expected = { + 'method': 'GET', + 'path': '/books/42', + 'query': '?format=hardcover', + 'headers': {'Content-type': 'application/json'}, + 'body': {'title': 'The Hitchhicker\'s Guide to the Galaxy'} +} + + +def build_request(): + request = Request('http://localhost') + request.method = 'POST' + request.selector = '/movies?format=PAL' + request.add_header('Content', 'silly') + request.data = {'title': 'A Fortune-Teller Told Me'} + return request + + +def test_non_matching_query(): + request = build_request() + msg = 'Non-matching query for the request. Expected:\n\n\t' + \ + str(expected['query']) + '\n\nReceived:\n\n\t' + '?format=PAL' + assert match(expected, request).value == msg + + +def test_non_matching_path(): + request = build_request() + request.selector = '/movies?format=hardcover' + msg = 'Non-matching path for the request. Expected:\n\n\t' + \ + str(expected['path']) + '\n\nReceived:\n\n\t' + '/movies' + assert match(expected, request).value == msg + + +def test_non_matching_method(): + request = build_request() + request.selector = '/books/42?format=hardcover' + msg = 'Non-matching method for the request. Expected:\n\n\t' + \ + str(expected['method']) + '\n\nReceived:\n\n\t' + 'POST' + assert match(expected, request).value == msg + + +def test_non_matching_dict_body(): + request = build_request() + request.selector = '/books/42?format=hardcover' + request.method = 'GET' + msg = 'Non-matching body for the request. Expected:\n\n\t' + \ + str(expected['body']) + '\n\nReceived:\n\n\t' + \ + str({'title': 'A Fortune-Teller Told Me'}) + assert match(expected, request).value == msg + + +def test_non_matching_text_body(): + request = build_request() + request.selector = '/books/42?format=hardcover' + request.method = 'GET' + request.data = 'Spam & Eggs' + expected['body'] = 'Eggs and Bacon' + msg = 'Non-matching body for the request. Expected:\n\n\t' + \ + str(expected['body']) + '\n\nReceived:\n\n\t' + \ + 'Spam & Eggs' + assert match(expected, request).value == msg + + +def test_non_matching_headers(): + request = build_request() + request.selector = '/books/42?format=hardcover' + request.method = 'GET' + request.data = {'title': 'The Hitchhicker\'s Guide to the Galaxy'} + expected['body'] = {'title': 'The Hitchhicker\'s Guide to the Galaxy'} + msg = 'Non-matching headers for the request. Expected:\n\n\t' + \ + str(expected['headers']) + '\n\nReceived:\n\n\t' + \ + str({'Content': 'silly'}) + assert match(expected, request).value == msg + + +def test_matching_request(): + request = build_request() + request.selector = '/books/42?format=hardcover' + request.method = 'GET' + request.data = {'title': 'The Hitchhicker\'s Guide to the Galaxy'} + request.add_header('Content-Type', 'application/json') + assert type(match(expected, request)) is Right diff --git a/tests/models/pact_helper.py b/tests/models/pact_helper.py new file mode 100644 index 0000000..ca3b9de --- /dev/null +++ b/tests/models/pact_helper.py @@ -0,0 +1,12 @@ +from pact_test import PactHelper + + +pact_helper = PactHelper() + + +def test_default_url(): + assert pact_helper.test_url == 'localhost' + + +def test_default_port(): + assert pact_helper.test_port == 9999 diff --git a/tests/models/service_consumer_test.py b/tests/models/service_consumer_test.py index 0dcc06b..7117564 100644 --- a/tests/models/service_consumer_test.py +++ b/tests/models/service_consumer_test.py @@ -1,7 +1,7 @@ -from pact_test.models.service_consumer_test import state -from pact_test.models.service_consumer_test import pact_uri -from pact_test.models.service_consumer_test import has_pact_with -from pact_test.models.service_consumer_test import ServiceConsumerTest +from pact_test import state +from pact_test import pact_uri +from pact_test import has_pact_with +from pact_test import ServiceConsumerTest def test_default_pact_uri(): diff --git a/tests/models/service_provider_test.py b/tests/models/service_provider_test.py index 89770d5..7155975 100644 --- a/tests/models/service_provider_test.py +++ b/tests/models/service_provider_test.py @@ -1,10 +1,10 @@ -from pact_test.models.service_provider_test import given -from pact_test.models.service_provider_test import with_request -from pact_test.models.service_provider_test import has_pact_with -from pact_test.models.service_provider_test import upon_receiving -from pact_test.models.service_provider_test import service_consumer -from pact_test.models.service_provider_test import will_respond_with -from pact_test.models.service_provider_test import ServiceProviderTest +from pact_test import given +from pact_test import with_request +from pact_test import has_pact_with +from pact_test import upon_receiving +from pact_test import service_consumer +from pact_test import will_respond_with +from pact_test import ServiceProviderTest def test_default_service_consumer_value(): diff --git a/tests/resources/pact_helper/pact_helper.py b/tests/resources/pact_helper/pact_helper.py index 6000793..06f61cd 100644 --- a/tests/resources/pact_helper/pact_helper.py +++ b/tests/resources/pact_helper/pact_helper.py @@ -1,6 +1,9 @@ -def set_up(): - print('Starting provider...') +from pact_test import PactHelper -def tear_down(): - print('Shutting down provider...') +class MyPactHelper(PactHelper): + def setup(self): + pass + + def tear_down(self): + pass diff --git a/tests/resources/pact_helper_no_setup/pact_helper.py b/tests/resources/pact_helper_no_setup/pact_helper.py index 0ff7fe3..545e180 100644 --- a/tests/resources/pact_helper_no_setup/pact_helper.py +++ b/tests/resources/pact_helper_no_setup/pact_helper.py @@ -1,2 +1,6 @@ -def tear_down(): - print('Shutting down provider...') +from pact_test import PactHelper + + +class MyPactHelper(PactHelper): + def tear_down(self): + pass diff --git a/tests/resources/pact_helper_no_tear_down/pact_helper.py b/tests/resources/pact_helper_no_tear_down/pact_helper.py index e00e960..d3089a1 100644 --- a/tests/resources/pact_helper_no_tear_down/pact_helper.py +++ b/tests/resources/pact_helper_no_tear_down/pact_helper.py @@ -1,2 +1,6 @@ -def setup(): - print('Starting provider...') +from pact_test import PactHelper + + +class MyPactHelper(PactHelper): + def setup(self): + pass diff --git a/tests/runners/service_consumers/state_test.py b/tests/runners/service_consumers/state_test.py index f5e7637..59a627c 100644 --- a/tests/runners/service_consumers/state_test.py +++ b/tests/runners/service_consumers/state_test.py @@ -1,64 +1,77 @@ -from pact_test.models.service_consumer_test import state -from pact_test.models.service_consumer_test import ServiceConsumerTest +from pact_test import state +from pact_test import PactHelper +from pact_test import ServiceConsumerTest from pact_test.runners.service_consumers.state_test import verify_state -from pact_test.runners.service_consumers.state_test import create_request -from pact_test.runners.service_consumers.state_test import build_test_result +from pact_test.runners.service_consumers.state_test import _create_request +from pact_test.runners.service_consumers.state_test import _parse_response -def test_build_test_result(): - status = 'Spam' - reason = 'Eggs' - expected_response = {'status': status, 'reason': reason} - assert build_test_result(status, reason) == expected_response +class MyPactHelper(PactHelper): + def setup(self): + pass + + def tear_down(self): + pass -def test_build_test_result_default(): - expected_response = {'status': 'PASSED', 'reason': None} - assert build_test_result() == expected_response +class TestLibraryApp(ServiceConsumerTest): + @state('some books exist') + def test_get_book(self): + pass + +test_instance = TestLibraryApp() +pact_helper = MyPactHelper() def test_verify_state(mocker): test_instance = TestLibraryApp() - pact_helper = PactHelper() + pact_helper = MyPactHelper() - mocker.spy(pact_helper, 'set_up') + mocker.spy(pact_helper, 'setup') mocker.spy(pact_helper, 'tear_down') response = verify_state(interaction, pact_helper, test_instance).value expected_response = {'status': 'PASSED', 'reason': None} assert response == expected_response - assert pact_helper.set_up.call_count == 1 + assert pact_helper.setup.call_count == 1 + assert pact_helper.tear_down.call_count == 1 assert pact_helper.tear_down.call_count == 1 def test_create_request(): request_body = interaction['request'] - request = create_request(request_body) + request = _create_request(pact_helper.test_url, pact_helper.test_port, + request_body).value assert request.method == request_body['method'] assert request.selector == request_body['path'] + request_body['query'] assert request.headers == request_body.get('headers', {}) -class PactHelper(object): - @staticmethod - def set_up(): - pass +def test_parse_text_response(): + request = _create_request('api.ipify.org', None, interaction['request']) + response = _parse_response(request.value).value + assert type(response) is str - @staticmethod - def tear_down(): - pass + +def test_parse_json_response(): + request = _create_request('ip.jsontest.com', None, interaction['request']) + response = _parse_response(request.value).value + assert type(response) is dict interaction = { 'providerState': 'some books exist', 'request': { 'method': 'GET', - 'path': '/books/42', - 'query': '?type=hardcover', + 'path': '', + 'query': '', 'headers': { 'Content-type': 'application/json' + }, + 'body': { + 'title': 'The Hitchhicker\'s Guide to the Galaxy' } }, 'response': { diff --git a/tests/runners/service_consumers/test_suite.py b/tests/runners/service_consumers/test_suite.py index 2867b80..e1c48aa 100644 --- a/tests/runners/service_consumers/test_suite.py +++ b/tests/runners/service_consumers/test_suite.py @@ -1,124 +1,125 @@ -import os -import sys -import imp -import json -from pact_test.config.config_builder import Config -from pact_test.runners.service_consumers.test_suite import ServiceConsumerTestSuiteRunner # nopep8 - - -def test_missing_pact_helper(): - config = Config() - t = ServiceConsumerTestSuiteRunner(config) - msg = 'Missing "pact_helper.py" at "tests/service_consumers".' - assert t.path_to_pact_helper().value == msg - - -def test_missing_setup_method(): - remove_pact_helper() - - config = Config() - test_pact_helper_path = os.path.join(os.getcwd(), 'tests', - 'resources', 'pact_helper_no_setup') - config.consumer_tests_path = test_pact_helper_path - t = ServiceConsumerTestSuiteRunner(config) - msg = 'Missing "setup" method in "pact_helper.py".' - assert t.load_pact_helper().value == msg - - -def test_missing_tear_down_method(): - remove_pact_helper() - - config = Config() - test_pact_helper_path = os.path.join(os.getcwd(), 'tests', 'resources', - 'pact_helper_no_tear_down') - config.consumer_tests_path = test_pact_helper_path - t = ServiceConsumerTestSuiteRunner(config) - msg = 'Missing "tear_down" method in "pact_helper.py".' - assert t.load_pact_helper().value == msg - - -def test_empty_tests_list(monkeypatch): - config = Config() - test_pact_helper_path = os.path.join(os.getcwd(), 'tests', 'resources', - 'service_consumers') - config.consumer_tests_path = test_pact_helper_path - - def empty_list(_): - return [] - monkeypatch.setattr(os, 'listdir', empty_list) - - t = ServiceConsumerTestSuiteRunner(config) - assert t.collect_tests().value == 'There are no consumer tests to verify.' - - -def test_collect_tests(): - config = Config() - test_pact_helper_path = os.path.join(os.getcwd(), 'tests', 'resources', - 'service_consumers') - config.consumer_tests_path = test_pact_helper_path - t = ServiceConsumerTestSuiteRunner(config) - - tests = t.collect_tests().value - assert len(tests) == 1 - - test = tests[0]() - assert test.pact_uri == 'http://google.com/' - assert test.has_pact_with == 'Restaurant' - - state = next(test.states) - assert state.state == 'the breakfast is available' - assert state() == 'Spam & Eggs' - - -def test_invalid_test(): - path = os.path.join(os.getcwd(), 'tests', 'resources', - 'invalid_service_consumer', 'customer.py') - module = imp.load_source('invalid_test', path) - test = module.TestRestaurantCustomer() - - t = ServiceConsumerTestSuiteRunner(None) - msg = 'Missing setup for "has_pact_with"' - assert t.verify_test(test).value.startswith(msg) - - -def test_verify_missing_state(mocker): - def pact_content(_): - s = '{"interactions": [{"providerState": "My State"}]}' - return json.loads(s) - - path = os.path.join(os.getcwd(), 'tests', 'resources', - 'service_consumers', 'test_restaurant_customer.py') - module = imp.load_source('consumer_test', path) - test = module.TestRestaurantCustomer() - - t = ServiceConsumerTestSuiteRunner(None) - mocker.patch.object(t, 'get_pact', new=pact_content) - - msg = 'Missing implementation for state "My State".' - assert t.verify_test(test).value == msg - - -def test_verify_existing_state(mocker): - def pact_content(_): - s = '{"interactions": [{"providerState": ' \ - '"the breakfast is available"}]}' - return json.loads(s) - - path = os.path.join(os.getcwd(), 'tests', 'resources', - 'service_consumers', 'test_restaurant_customer.py') - module = imp.load_source('consumer_test', path) - test = module.TestRestaurantCustomer() - - t = ServiceConsumerTestSuiteRunner(None) - mocker.patch.object(t, 'get_pact', new=pact_content) - mocker.spy(t, 'verify_state') - - t.verify_test(test) - assert t.verify_state.call_count == 1 - - -def remove_pact_helper(): - try: - del sys.modules['pact_helper'] - except KeyError: - pass +# import os +# import sys +# import imp +# import json +# from pact_test.config.config_builder import Config +# from pact_test.runners.service_consumers.test_suite import ServiceConsumerTestSuiteRunner # nopep8 +# +# +# def test_missing_pact_helper(): +# config = Config() +# t = ServiceConsumerTestSuiteRunner(config) +# msg = 'Missing "pact_helper.py" at "tests/service_consumers".' +# assert t.path_to_pact_helper().value == msg +# +# +# def test_missing_setup_method(): +# remove_pact_helper() +# +# config = Config() +# test_pact_helper_path = os.path.join(os.getcwd(), 'tests', +# 'resources', 'pact_helper_no_setup') +# config.consumer_tests_path = test_pact_helper_path +# t = ServiceConsumerTestSuiteRunner(config) +# msg = 'Missing "setup" method in "pact_helper.py".' +# assert t.load_pact_helper().value == msg +# +# +# def test_missing_tear_down_method(): +# remove_pact_helper() +# +# config = Config() +# test_pact_helper_path = os.path.join(os.getcwd(), 'tests', 'resources', +# 'pact_helper_no_tear_down') +# config.consumer_tests_path = test_pact_helper_path +# t = ServiceConsumerTestSuiteRunner(config) +# msg = 'Missing "tear_down" method in "pact_helper.py".' +# assert t.load_pact_helper().value == msg +# +# +# def test_empty_tests_list(monkeypatch): +# config = Config() +# test_pact_helper_path = os.path.join(os.getcwd(), 'tests', 'resources', +# 'service_consumers') +# config.consumer_tests_path = test_pact_helper_path +# +# def empty_list(_): +# return [] +# monkeypatch.setattr(os, 'listdir', empty_list) +# +# t = ServiceConsumerTestSuiteRunner(config) +# msg = 'There are no consumer tests to verify.' +# assert t.collect_tests().value == msg +# +# +# def test_collect_tests(): +# config = Config() +# test_pact_helper_path = os.path.join(os.getcwd(), 'tests', 'resources', +# 'service_consumers') +# config.consumer_tests_path = test_pact_helper_path +# t = ServiceConsumerTestSuiteRunner(config) +# +# tests = t.collect_tests().value +# assert len(tests) == 1 +# +# test = tests[0]() +# assert test.pact_uri == 'http://google.com/' +# assert test.has_pact_with == 'Restaurant' +# +# state = next(test.states) +# assert state.state == 'the breakfast is available' +# assert state() == 'Spam & Eggs' +# +# +# def test_invalid_test(): +# path = os.path.join(os.getcwd(), 'tests', 'resources', +# 'invalid_service_consumer', 'customer.py') +# module = imp.load_source('invalid_test', path) +# test = module.TestRestaurantCustomer() +# +# t = ServiceConsumerTestSuiteRunner(None) +# msg = 'Missing setup for "has_pact_with"' +# assert t.verify_test(test).value.startswith(msg) +# +# +# def test_verify_missing_state(mocker): +# def pact_content(_): +# s = '{"interactions": [{"providerState": "My State"}]}' +# return json.loads(s) +# +# path = os.path.join(os.getcwd(), 'tests', 'resources', +# 'service_consumers', 'test_restaurant_customer.py') +# module = imp.load_source('consumer_test', path) +# test = module.TestRestaurantCustomer() +# +# t = ServiceConsumerTestSuiteRunner(None) +# mocker.patch.object(t, 'get_pact', new=pact_content) +# +# msg = 'Missing implementation for state "My State".' +# assert t.verify_test(test).value == msg +# +# +# def test_verify_existing_state(mocker): +# def pact_content(_): +# s = '{"interactions": [{"providerState": ' \ +# '"the breakfast is available"}]}' +# return json.loads(s) +# +# path = os.path.join(os.getcwd(), 'tests', 'resources', +# 'service_consumers', 'test_restaurant_customer.py') +# module = imp.load_source('consumer_test', path) +# test = module.TestRestaurantCustomer() +# +# t = ServiceConsumerTestSuiteRunner(None) +# mocker.patch.object(t, 'get_pact', new=pact_content) +# mocker.spy(t, 'verify_state') +# +# t.verify_test(test) +# assert t.verify_state.call_count == 1 +# +# +# def remove_pact_helper(): +# try: +# del sys.modules['pact_helper'] +# except KeyError: +# pass diff --git a/tests/utils/pact_helper_utils.py b/tests/utils/pact_helper_utils.py new file mode 100644 index 0000000..e0e32b1 --- /dev/null +++ b/tests/utils/pact_helper_utils.py @@ -0,0 +1,53 @@ +import os +from pact_test import PactHelper +from pact_test.utils.pact_helper_utils import load_pact_helper + + +def test_path_to_pact_helper(monkeypatch): + monkeypatch.setattr(os.path, 'isfile', lambda _: True) + + consumer_tests_path = '/consumer_tests/' + pact_helper = load_pact_helper(consumer_tests_path).value + + msg = 'Missing "pact_helper.py" at "/consumer_tests/pact_helper.py".' + assert pact_helper == msg + + +def test_path_to_pact_helper_missing(): + consumer_tests_path = '/consumer_tests/' + pact_helper = load_pact_helper(consumer_tests_path).value + + msg = 'Missing "pact_helper.py" at "/consumer_tests/".' + assert pact_helper == msg + + +def test_load_module_missing(): + consumer_tests_path = '/consumer_tests/' + pact_helper = load_pact_helper(consumer_tests_path).value + + assert pact_helper == 'Missing "pact_helper.py" at "/consumer_tests/".' + + +def test_load_user_class_missing_setup(): + consumer_tests_path = os.path.join(os.getcwd(), 'tests', + 'resources', 'pact_helper_no_setup') + pact_helper = load_pact_helper(consumer_tests_path).value + + error_message = 'Missing "setup" method in "pact_helper.py".' + assert pact_helper == error_message + + +def test_load_user_class_missing_tear_down(): + consumer_tests_path = os.path.join(os.getcwd(), 'tests', + 'resources', 'pact_helper_no_tear_down') + pact_helper = load_pact_helper(consumer_tests_path).value + + error_message = 'Missing "tear_down" method in "pact_helper.py".' + assert pact_helper == error_message + + +def test_load_pact_helper(): + consumer_tests_path = os.path.join(os.getcwd(), 'tests', + 'resources', 'pact_helper') + pact_helper = load_pact_helper(consumer_tests_path).value + assert issubclass(pact_helper.__class__, PactHelper) From a64a609112a7214cb56fae770b464a836ba684b9 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Mon, 12 Jun 2017 10:16:16 +1000 Subject: [PATCH 20/85] Custom models for Request and Response. --- pact_test/models/request.py | 13 +++++++ pact_test/models/response.py | 9 +++++ tests/models/request.py | 70 ++++++++++++++++++++++++++++++++++++ tests/models/response.py | 44 +++++++++++++++++++++++ 4 files changed, 136 insertions(+) create mode 100644 pact_test/models/request.py create mode 100644 pact_test/models/response.py create mode 100644 tests/models/request.py create mode 100644 tests/models/response.py diff --git a/pact_test/models/request.py b/pact_test/models/request.py new file mode 100644 index 0000000..8660ccc --- /dev/null +++ b/pact_test/models/request.py @@ -0,0 +1,13 @@ +class PactRequest(object): + path = '' + query = '' + body = None + headers = [] + method = 'GET' + + def __init__(self, method=None, body=None, headers=None, path=None, query=None): + self.body = body or self.body + self.path = path or self.path + self.query = query or self.query + self.method = method or self.method + self.headers = headers or self.headers diff --git a/pact_test/models/response.py b/pact_test/models/response.py new file mode 100644 index 0000000..ba8c0df --- /dev/null +++ b/pact_test/models/response.py @@ -0,0 +1,9 @@ +class PactResponse(object): + body = None + headers = [] + status = 500 + + def __init__(self, status=None, body=None, headers=None): + self.body = body or self.body + self.status = status or self.status + self.headers = headers or self.headers diff --git a/tests/models/request.py b/tests/models/request.py new file mode 100644 index 0000000..f77a02b --- /dev/null +++ b/tests/models/request.py @@ -0,0 +1,70 @@ +from pact_test.models.request import PactRequest + + +def test_default_body(): + r = PactRequest() + assert r.body is None + + +def test_default_headers(): + r = PactRequest() + assert r.headers == [] + + +def test_default_method(): + r = PactRequest() + assert r.method == 'GET' + + +def test_default_path(): + r = PactRequest() + assert r.path == '' + + +def test_default_query(): + r = PactRequest() + assert r.query == '' + + +def test_custom_query(): + r = PactRequest(query='?type=spam') + assert r.query == '?type=spam' + + r.query = '?type=eggs' + assert r.query == '?type=eggs' + + +def test_custom_path(): + r = PactRequest(path='/spam/') + assert r.path == '/spam/' + + r.path = '/eggs/' + assert r.path == '/eggs/' + + +def test_custom_method(): + r = PactRequest(method='POST') + assert r.method == 'POST' + + r.method = 'PUT' + assert r.method == 'PUT' + + +def test_custom_headers(): + headers = [('Content-Type', 'text')] + r = PactRequest(headers=headers) + assert r.headers == headers + + headers = [('Content-Type', 'application/json')] + r.headers = headers + assert r.headers == headers + + +def test_custom_body(): + body = {'spam': 'eggs'} + r = PactRequest(body=body) + assert r.body == body + + body = {'spam': 'spam'} + r.body = body + assert r.body == body diff --git a/tests/models/response.py b/tests/models/response.py new file mode 100644 index 0000000..3b65a82 --- /dev/null +++ b/tests/models/response.py @@ -0,0 +1,44 @@ +from pact_test.models.response import PactResponse + + +def test_default_body(): + r = PactResponse() + assert r.body is None + + +def test_deafult_headers(): + r = PactResponse() + assert r.headers == [] + + +def test_default_status(): + r = PactResponse() + assert r.status == 500 + + +def test_custom_body(): + body = {'spam': 'eggs'} + r = PactResponse(body=body) + assert r.body == body + + body = {'spam': 'spam'} + r.body = body + assert r.body == body + + +def test_custom_headers(): + headers = [('Content-Type', 'text')] + r = PactResponse(headers=headers) + assert r.headers == headers + + headers = [('Content-Type', 'application/json')] + r.headers = headers + assert r.headers == headers + + +def test_custom_status(): + r = PactResponse(status=200) + assert r.status == 200 + + r.status = 201 + assert r.status == 201 From b406386fcb9fb79b5728460c03360762676727b5 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Mon, 12 Jun 2017 10:47:45 +1000 Subject: [PATCH 21/85] Build request from Pact file. --- pact_test/utils/http.py | 19 +++++++++++ tests/utils/http.py | 73 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 pact_test/utils/http.py create mode 100644 tests/utils/http.py diff --git a/pact_test/utils/http.py b/pact_test/utils/http.py new file mode 100644 index 0000000..27b7ded --- /dev/null +++ b/pact_test/utils/http.py @@ -0,0 +1,19 @@ +from pact_test.models.request import PactRequest + + +def build_request_from_pact(pact): + return PactRequest( + body=pact['request'].get('body', None), + path=pact['request'].get('path', None), + query=pact['request'].get('query', None), + method=pact['request'].get('method', None), + headers=_build_request_headers_from_pact(pact), + ) + + +def _build_request_headers_from_pact(pact): + headers = [] + pact_headers = pact['request'].get('headers', None) + for h in (h for h in (pact_headers if pact_headers else [])): + headers.append((h, pact_headers[h])) + return headers diff --git a/tests/utils/http.py b/tests/utils/http.py new file mode 100644 index 0000000..ea92b36 --- /dev/null +++ b/tests/utils/http.py @@ -0,0 +1,73 @@ +from pact_test.utils.http import build_request_from_pact + + +def test_request_method(): + pact = {'request': {'method': 'POST'}} + request = build_request_from_pact(pact) + + assert request.method == 'POST' + + +def test_request_missing_method(): + pact = {'request': {}} + request = build_request_from_pact(pact) + + assert request.method == 'GET' + + +def test_request_path(): + pact = {'request': {'path': '/spam'}} + request = build_request_from_pact(pact) + + assert request.path == '/spam' + + +def test_request_missing_path(): + pact = {'request': {}} + request = build_request_from_pact(pact) + + assert request.path == '' + + +def test_request_query(): + pact = {'request': {'query': '?spam=eggs'}} + request = build_request_from_pact(pact) + + assert request.query == '?spam=eggs' + + +def test_request_missing_query(): + pact = {'request': {}} + request = build_request_from_pact(pact) + + assert request.query == '' + + +def test_request_headers(): + headers = {'Content-Type': 'application/json'} + pact = {'request': {'headers': headers}} + request = build_request_from_pact(pact) + + assert request.headers == [('Content-Type', 'application/json')] + + +def test_request_missing_headers(): + pact = {'request': {}} + request = build_request_from_pact(pact) + + assert request.headers == [] + + +def test_request_body(): + body = {'spam': 'eggs'} + pact = {'request': {'body': body}} + request = build_request_from_pact(pact) + + assert request.body == body + + +def test_request_missing_body(): + pact = {'request': {}} + request = build_request_from_pact(pact) + + assert request.body is None From 13a671e1ab27fe6bfb1510ecde41881a77798673 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Mon, 12 Jun 2017 11:10:24 +1000 Subject: [PATCH 22/85] Build response from Pact file. --- pact_test/utils/http.py | 33 ++++++++++++---- tests/utils/http.py | 87 +++++++++++++++++++++++++++++++---------- 2 files changed, 91 insertions(+), 29 deletions(-) diff --git a/pact_test/utils/http.py b/pact_test/utils/http.py index 27b7ded..20d5cb2 100644 --- a/pact_test/utils/http.py +++ b/pact_test/utils/http.py @@ -1,19 +1,36 @@ from pact_test.models.request import PactRequest +from pact_test.models.response import PactResponse -def build_request_from_pact(pact): +REQUEST = 'request' +RESPONSE = 'response' + + +def build_request_from_interaction(interaction): + request = interaction[REQUEST] + return PactRequest( - body=pact['request'].get('body', None), - path=pact['request'].get('path', None), - query=pact['request'].get('query', None), - method=pact['request'].get('method', None), - headers=_build_request_headers_from_pact(pact), + body=request.get('body', None), + path=request.get('path', None), + query=request.get('query', None), + method=request.get('method', None), + headers=_build_headers_from_pact(interaction, REQUEST) + ) + + +def build_response_from_interaction(interaction): + response = interaction[RESPONSE] + + return PactResponse( + body=response.get('body', None), + status=response.get('status', None), + headers=_build_headers_from_pact(interaction, RESPONSE) ) -def _build_request_headers_from_pact(pact): +def _build_headers_from_pact(pact, request_or_response): headers = [] - pact_headers = pact['request'].get('headers', None) + pact_headers = pact[request_or_response].get('headers', None) for h in (h for h in (pact_headers if pact_headers else [])): headers.append((h, pact_headers[h])) return headers diff --git a/tests/utils/http.py b/tests/utils/http.py index ea92b36..cee84b3 100644 --- a/tests/utils/http.py +++ b/tests/utils/http.py @@ -1,73 +1,118 @@ -from pact_test.utils.http import build_request_from_pact +from pact_test.utils.http import build_request_from_interaction +from pact_test.utils.http import build_response_from_interaction + + +def test_response_status(): + interaction = {'response': {'status': 200}} + response = build_response_from_interaction(interaction) + + assert response.status == 200 + + +def test_response_missing_status(): + interaction = {'response': {}} + response = build_response_from_interaction(interaction) + + assert response.status == 500 + + +def test_response_body(): + body = {'spam': 'eggs'} + interaction = {'response': {'body': body}} + response = build_response_from_interaction(interaction) + + assert response.body == body + + +def test_response_missing_body(): + interaction = {'response': {}} + response = build_response_from_interaction(interaction) + + assert response.body is None + + +def test_response_headers(): + headers = {'Content-Type': 'spam'} + interaction = {'response': {'headers': headers}} + response = build_response_from_interaction(interaction) + + assert response.headers == [('Content-Type', 'spam')] + + +def test_response_missing_headers(): + interaction = {'response': {}} + response = build_response_from_interaction(interaction) + + assert response.headers == [] def test_request_method(): - pact = {'request': {'method': 'POST'}} - request = build_request_from_pact(pact) + interaction = {'request': {'method': 'POST'}} + request = build_request_from_interaction(interaction) assert request.method == 'POST' def test_request_missing_method(): - pact = {'request': {}} - request = build_request_from_pact(pact) + interaction = {'request': {}} + request = build_request_from_interaction(interaction) assert request.method == 'GET' def test_request_path(): - pact = {'request': {'path': '/spam'}} - request = build_request_from_pact(pact) + interaction = {'request': {'path': '/spam'}} + request = build_request_from_interaction(interaction) assert request.path == '/spam' def test_request_missing_path(): - pact = {'request': {}} - request = build_request_from_pact(pact) + interaction = {'request': {}} + request = build_request_from_interaction(interaction) assert request.path == '' def test_request_query(): - pact = {'request': {'query': '?spam=eggs'}} - request = build_request_from_pact(pact) + interaction = {'request': {'query': '?spam=eggs'}} + request = build_request_from_interaction(interaction) assert request.query == '?spam=eggs' def test_request_missing_query(): - pact = {'request': {}} - request = build_request_from_pact(pact) + interaction = {'request': {}} + request = build_request_from_interaction(interaction) assert request.query == '' def test_request_headers(): headers = {'Content-Type': 'application/json'} - pact = {'request': {'headers': headers}} - request = build_request_from_pact(pact) + interaction = {'request': {'headers': headers}} + request = build_request_from_interaction(interaction) assert request.headers == [('Content-Type', 'application/json')] def test_request_missing_headers(): - pact = {'request': {}} - request = build_request_from_pact(pact) + interaction = {'request': {}} + request = build_request_from_interaction(interaction) assert request.headers == [] def test_request_body(): body = {'spam': 'eggs'} - pact = {'request': {'body': body}} - request = build_request_from_pact(pact) + interaction = {'request': {'body': body}} + request = build_request_from_interaction(interaction) assert request.body == body def test_request_missing_body(): - pact = {'request': {}} - request = build_request_from_pact(pact) + interaction = {'request': {}} + request = build_request_from_interaction(interaction) assert request.body is None From db9b321d52e333ebb2b0483d6e6c764fe3555090 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Mon, 12 Jun 2017 14:31:47 +1000 Subject: [PATCH 23/85] Custom HTTP client. --- pact_test/clients/__init__.py | 0 pact_test/clients/http_client.py | 80 ++++++++++++++++++++++ pact_test/utils/{http.py => http_utils.py} | 0 tests/clients/http_client.py | 65 ++++++++++++++++++ tests/utils/{http.py => http_utils.py} | 4 +- tox.ini | 3 +- 6 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 pact_test/clients/__init__.py create mode 100644 pact_test/clients/http_client.py rename pact_test/utils/{http.py => http_utils.py} (100%) create mode 100644 tests/clients/http_client.py rename tests/utils/{http.py => http_utils.py} (95%) diff --git a/pact_test/clients/__init__.py b/pact_test/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pact_test/clients/http_client.py b/pact_test/clients/http_client.py new file mode 100644 index 0000000..b7836c6 --- /dev/null +++ b/pact_test/clients/http_client.py @@ -0,0 +1,80 @@ +import json +from pact_test.models.response import PactResponse + +from pact_test.utils.http_utils import build_request_from_interaction +try: # pragma: no cover + from urllib.request import Request, urlopen # pragma: no cover +except: # pragma: no cover + from urllib2 import Request, urlopen # pragma: no cover + + +TEXT = 'text' + + +def execute_interaction_request(url, port, interaction): + request = _pact2python_request(url, port, interaction) + return _python2pact_response(urlopen(request)) + + +def _python2pact_response(response): + status = _parse_status(response) + headers = _parse_headers(response) + content_type = _get_content_type(headers) + + return PactResponse( + headers=headers, + status=status, + body=_parse_body(response, content_type) + ) + + +def _parse_status(response): + try: + status = response.status + except AttributeError: + status = response.code + return status + + +def _parse_headers(response): + try: + headers = response.getheaders() + except AttributeError: + headers = [] + for h in response.headers.headers: + key_value = h.split(':') + key = key_value[0].strip() + val = key_value[1].strip() + headers.append((key, val)) + return headers + + +def _parse_body(response, content_type): + body = response.read().decode('utf-8') + if content_type == TEXT: + return body + return json.loads(body) + + +def _get_content_type(headers): + content_type = TEXT + types = list(filter(lambda h: h[0].upper() == 'CONTENT-TYPE', headers)) + if types: + content_type = types[0][1] + return content_type + + +def _pact2python_request(url, port, interaction): + pact_request = build_request_from_interaction(interaction) + + request_port = str(port) if port else '80' + request_url = 'http://' + url + ':' + request_port + + request = Request(request_url) + request.data = bytearray().extend(map(ord, json.dumps(pact_request.body))) + request.method = pact_request.method + for key, value in pact_request.headers: + request.add_header(key, value) + request.selector = pact_request.path + pact_request.query + + return request diff --git a/pact_test/utils/http.py b/pact_test/utils/http_utils.py similarity index 100% rename from pact_test/utils/http.py rename to pact_test/utils/http_utils.py diff --git a/tests/clients/http_client.py b/tests/clients/http_client.py new file mode 100644 index 0000000..4b4895d --- /dev/null +++ b/tests/clients/http_client.py @@ -0,0 +1,65 @@ +import json +from pact_test.models.request import PactRequest +from pact_test.clients.http_client import _parse_body +from pact_test.clients.http_client import _get_content_type +from pact_test.clients.http_client import _pact2python_request +from pact_test.clients.http_client import execute_interaction_request +try: # pragma: no cover + from urllib.request import Request, urlopen # pragma: no cover +except: # pragma: no cover + from urllib2 import Request, urlopen # pragma: no cover + + +def test_execute_interaction_request(mocker): + url = 'echo.jsontest.com' + port = None + interaction = {'request': {'path': '/spam/eggs'}} + mocker.spy(PactRequest, '__init__') + response = execute_interaction_request(url, port, interaction) + + assert PactRequest.__init__.call_count == 1 + assert response.status == 200 + assert len(response.headers) > 1 + assert response.body == {'spam': 'eggs'} + + +def test_get_content_type(): + headers = [ + ('Server', 'Spam'), + ('Content-Type', 'application/json'), + ('Date', '12-06-2017') + ] + content_type = _get_content_type(headers) + + assert content_type == 'application/json' + + +def test_get_default_content_type(): + headers = [ + ('Server', 'Spam'), + ('Date', '12-06-2017') + ] + content_type = _get_content_type(headers) + + assert content_type == 'text' + + +def test_pact2python_request(): + url = 'localhost' + port = 9999 + body = {'spam': 'eggs'} + headers = {'Content-type': 'spam', 'Date': 'today'} + interaction = { + 'request': { + 'method': 'POST', + 'path': '/spam', + 'query': '?spam=eggs', + 'body': body, + 'headers': headers + } + } + request = _pact2python_request(url, port, interaction) + assert request.method == 'POST' + assert request.selector == '/spam?spam=eggs' + assert request.headers == headers + assert request.data == bytearray().extend(map(ord, json.dumps(body))) diff --git a/tests/utils/http.py b/tests/utils/http_utils.py similarity index 95% rename from tests/utils/http.py rename to tests/utils/http_utils.py index cee84b3..b1aa699 100644 --- a/tests/utils/http.py +++ b/tests/utils/http_utils.py @@ -1,5 +1,5 @@ -from pact_test.utils.http import build_request_from_interaction -from pact_test.utils.http import build_response_from_interaction +from pact_test.utils.http_utils import build_request_from_interaction +from pact_test.utils.http_utils import build_response_from_interaction def test_response_status(): diff --git a/tox.ini b/tox.ini index 34871a6..2e998f5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,7 @@ [tox] #envlist = py27, py33, py34, py35, py36 envlist = py27, py36 +;envlist = py27 [testenv] deps = @@ -21,4 +22,4 @@ passenv = TRAVIS_JOB_ID TRAVIS_REPO_SLUG TRAVIS_COMMIT -commands = py.test --cov pact_test --cov-report term-missing +commands = py.test --cov pact_test --cov-report term-missing -p no:sugar From 98cdfc4e0dce1f9471b045ebe314b3981af1e1b0 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Mon, 12 Jun 2017 15:40:08 +1000 Subject: [PATCH 24/85] Switch to Requests: HTTP for Humans --- pact_test/clients/http_client.py | 81 ++++++---------- requirements.txt | 1 + setup.py | 2 +- tests/clients/http_client.py | 160 +++++++++++++++++++++---------- 4 files changed, 137 insertions(+), 107 deletions(-) diff --git a/pact_test/clients/http_client.py b/pact_test/clients/http_client.py index b7836c6..918df4d 100644 --- a/pact_test/clients/http_client.py +++ b/pact_test/clients/http_client.py @@ -1,59 +1,48 @@ -import json +import requests from pact_test.models.response import PactResponse -from pact_test.utils.http_utils import build_request_from_interaction -try: # pragma: no cover - from urllib.request import Request, urlopen # pragma: no cover -except: # pragma: no cover - from urllib2 import Request, urlopen # pragma: no cover - TEXT = 'text' +JSON = 'application/json' def execute_interaction_request(url, port, interaction): - request = _pact2python_request(url, port, interaction) - return _python2pact_response(urlopen(request)) - + url = _build_url(url, port, interaction) -def _python2pact_response(response): - status = _parse_status(response) - headers = _parse_headers(response) + server_response = requests.request('GET', url=url) + headers = _parse_headers(server_response) content_type = _get_content_type(headers) return PactResponse( + status=server_response.status_code, headers=headers, - status=status, - body=_parse_body(response, content_type) + body=_parse_body(server_response, content_type) ) -def _parse_status(response): - try: - status = response.status - except AttributeError: - status = response.code - return status - - -def _parse_headers(response): - try: - headers = response.getheaders() - except AttributeError: - headers = [] - for h in response.headers.headers: - key_value = h.split(':') - key = key_value[0].strip() - val = key_value[1].strip() - headers.append((key, val)) +def _parse_body(server_response, content_type): + if JSON in content_type: + return server_response.json() + else: + return server_response.text() + + +def _parse_headers(server_response): + headers = [] + server_headers = server_response.headers + for key in server_headers: + headers.append((key, server_headers[key])) return headers -def _parse_body(response, content_type): - body = response.read().decode('utf-8') - if content_type == TEXT: - return body - return json.loads(body) +def _build_url(url, port, interaction): + path = interaction['request'].get('path', '') + query = interaction['request'].get('query', '') + + test_port = str(port) if port else str(80) + test_url = 'http://' + url + ':' + test_port + path + query + + return test_url def _get_content_type(headers): @@ -62,19 +51,3 @@ def _get_content_type(headers): if types: content_type = types[0][1] return content_type - - -def _pact2python_request(url, port, interaction): - pact_request = build_request_from_interaction(interaction) - - request_port = str(port) if port else '80' - request_url = 'http://' + url + ':' + request_port - - request = Request(request_url) - request.data = bytearray().extend(map(ord, json.dumps(pact_request.body))) - request.method = pact_request.method - for key, value in pact_request.headers: - request.add_header(key, value) - request.selector = pact_request.path + pact_request.query - - return request diff --git a/requirements.txt b/requirements.txt index f650fda..5620863 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +requests pytest>=3.0 pytest-pep8 pytest-mock diff --git a/setup.py b/setup.py index 783dc4a..0cd550a 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ license='LICENSE', long_description=open('README.rst').read(), description='Python implementation for Pact (http://pact.io/)', - install_requires=[], + install_requires=['requests'], setup_requires=['pytest-runner'], tests_require=[ 'pytest>=3.0', diff --git a/tests/clients/http_client.py b/tests/clients/http_client.py index 4b4895d..5911199 100644 --- a/tests/clients/http_client.py +++ b/tests/clients/http_client.py @@ -1,65 +1,121 @@ -import json -from pact_test.models.request import PactRequest -from pact_test.clients.http_client import _parse_body -from pact_test.clients.http_client import _get_content_type -from pact_test.clients.http_client import _pact2python_request +import requests +from pact_test.clients.http_client import _build_url +from pact_test.clients.http_client import _parse_headers from pact_test.clients.http_client import execute_interaction_request -try: # pragma: no cover - from urllib.request import Request, urlopen # pragma: no cover -except: # pragma: no cover - from urllib2 import Request, urlopen # pragma: no cover def test_execute_interaction_request(mocker): - url = 'echo.jsontest.com' + class Response(object): + status_code = 200 + headers = {'Content-Type': 'application/json', 'Date': '12-06-2017'} + + def json(self): + return {'spam': 'eggs'} + + mocker.patch.object(requests, 'request', lambda x, **kwargs: Response()) + url = 'montypython.com' port = None interaction = {'request': {'path': '/spam/eggs'}} - mocker.spy(PactRequest, '__init__') response = execute_interaction_request(url, port, interaction) - assert PactRequest.__init__.call_count == 1 assert response.status == 200 assert len(response.headers) > 1 assert response.body == {'spam': 'eggs'} -def test_get_content_type(): - headers = [ - ('Server', 'Spam'), - ('Content-Type', 'application/json'), - ('Date', '12-06-2017') - ] - content_type = _get_content_type(headers) - - assert content_type == 'application/json' - - -def test_get_default_content_type(): - headers = [ - ('Server', 'Spam'), - ('Date', '12-06-2017') - ] - content_type = _get_content_type(headers) - - assert content_type == 'text' - - -def test_pact2python_request(): - url = 'localhost' - port = 9999 - body = {'spam': 'eggs'} - headers = {'Content-type': 'spam', 'Date': 'today'} - interaction = { - 'request': { - 'method': 'POST', - 'path': '/spam', - 'query': '?spam=eggs', - 'body': body, - 'headers': headers - } - } - request = _pact2python_request(url, port, interaction) - assert request.method == 'POST' - assert request.selector == '/spam?spam=eggs' - assert request.headers == headers - assert request.data == bytearray().extend(map(ord, json.dumps(body))) +def test_execute_interaction_request_text(mocker): + class Response(object): + status_code = 200 + headers = {'Date': '12-06-2017'} + + def text(self): + return 'Spam & Eggs' + + mocker.patch.object(requests, 'request', lambda x, **kwargs: Response()) + url = 'montypython.com' + port = None + interaction = {'request': {'path': '/spam/eggs'}} + response = execute_interaction_request(url, port, interaction) + + assert response.status == 200 + assert response.headers == [('Date', '12-06-2017')] + assert response.body == 'Spam & Eggs' + + +def test_parse_headers(): + headers = [('Content-Type', 'spam')] + server_headers = {'Content-Type': 'spam'} + + class Response(object): + headers = server_headers + + assert _parse_headers(Response()) == headers + + +def test_build_url(): + url = 'montypython.com' + port = None + interaction = {'request': {}} + url = _build_url(url, port, interaction) + + assert url == 'http://montypython.com:80' + + +def test_build_url_with_path(): + url = 'montypython.com' + port = None + interaction = {'request': {'path': '/spam/eggs'}} + url = _build_url(url, port, interaction) + + assert url == 'http://montypython.com:80/spam/eggs' + + +def test_build_url_with_query(): + url = 'montypython.com' + port = None + interaction = {'request': {'query': '?spam=eggs'}} + url = _build_url(url, port, interaction) + + assert url == 'http://montypython.com:80?spam=eggs' + + +# def test_get_content_type(): +# headers = [ +# ('Server', 'Spam'), +# ('Content-Type', 'application/json'), +# ('Date', '12-06-2017') +# ] +# content_type = _get_content_type(headers) +# +# assert content_type == 'application/json' +# +# +# def test_get_default_content_type(): +# headers = [ +# ('Server', 'Spam'), +# ('Date', '12-06-2017') +# ] +# content_type = _get_content_type(headers) +# +# assert content_type == 'text' +# +# +# def test_pact2python_request(): +# url = 'localhost' +# port = 9999 +# body = {'spam': 'eggs'} +# headers = {'Content-type': 'spam', 'Date': 'today'} +# interaction = { +# 'request': { +# 'method': 'POST', +# 'path': '/spam', +# 'query': '?spam=eggs', +# 'body': body, +# 'headers': headers +# } +# } +# request = _pact2python_request(url, port, interaction) +# assert request.method == 'POST' +# assert request.selector == '/spam?spam=eggs' +# assert request.headers == headers +# assert request.data == bytearray().extend(map(ord, json.dumps(body))) From cd65e2cbb8b7d6046aa3857a61e52d55d58aefa7 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Mon, 12 Jun 2017 15:48:03 +1000 Subject: [PATCH 25/85] WIP --- tests/clients/http_client.py | 42 ------------------- tests/runners/service_consumers/state_test.py | 22 ---------- 2 files changed, 64 deletions(-) diff --git a/tests/clients/http_client.py b/tests/clients/http_client.py index 5911199..385eb32 100644 --- a/tests/clients/http_client.py +++ b/tests/clients/http_client.py @@ -77,45 +77,3 @@ def test_build_url_with_query(): url = _build_url(url, port, interaction) assert url == 'http://montypython.com:80?spam=eggs' - - -# def test_get_content_type(): -# headers = [ -# ('Server', 'Spam'), -# ('Content-Type', 'application/json'), -# ('Date', '12-06-2017') -# ] -# content_type = _get_content_type(headers) -# -# assert content_type == 'application/json' -# -# -# def test_get_default_content_type(): -# headers = [ -# ('Server', 'Spam'), -# ('Date', '12-06-2017') -# ] -# content_type = _get_content_type(headers) -# -# assert content_type == 'text' -# -# -# def test_pact2python_request(): -# url = 'localhost' -# port = 9999 -# body = {'spam': 'eggs'} -# headers = {'Content-type': 'spam', 'Date': 'today'} -# interaction = { -# 'request': { -# 'method': 'POST', -# 'path': '/spam', -# 'query': '?spam=eggs', -# 'body': body, -# 'headers': headers -# } -# } -# request = _pact2python_request(url, port, interaction) -# assert request.method == 'POST' -# assert request.selector == '/spam?spam=eggs' -# assert request.headers == headers -# assert request.data == bytearray().extend(map(ord, json.dumps(body))) diff --git a/tests/runners/service_consumers/state_test.py b/tests/runners/service_consumers/state_test.py index 59a627c..f4065db 100644 --- a/tests/runners/service_consumers/state_test.py +++ b/tests/runners/service_consumers/state_test.py @@ -39,28 +39,6 @@ def test_verify_state(mocker): assert pact_helper.tear_down.call_count == 1 -def test_create_request(): - request_body = interaction['request'] - request = _create_request(pact_helper.test_url, pact_helper.test_port, - request_body).value - - assert request.method == request_body['method'] - assert request.selector == request_body['path'] + request_body['query'] - assert request.headers == request_body.get('headers', {}) - - -def test_parse_text_response(): - request = _create_request('api.ipify.org', None, interaction['request']) - response = _parse_response(request.value).value - assert type(response) is str - - -def test_parse_json_response(): - request = _create_request('ip.jsontest.com', None, interaction['request']) - response = _parse_response(request.value).value - assert type(response) is dict - - interaction = { 'providerState': 'some books exist', 'request': { From 8edd20a0c687baaf9579f946127c740e79f6507a Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Mon, 12 Jun 2017 16:01:19 +1000 Subject: [PATCH 26/85] WIP --- .../runners/service_consumers/state_test.py | 56 ++++--------------- tests/runners/service_consumers/state_test.py | 12 +++- 2 files changed, 22 insertions(+), 46 deletions(-) diff --git a/pact_test/runners/service_consumers/state_test.py b/pact_test/runners/service_consumers/state_test.py index 1bf3f34..6c22059 100644 --- a/pact_test/runners/service_consumers/state_test.py +++ b/pact_test/runners/service_consumers/state_test.py @@ -1,55 +1,23 @@ -import json from pact_test.either import * -try: # pragma: no cover - from urllib.request import Request, urlopen # pragma: no cover -except: # pragma: no cover - from urllib2 import Request, urlopen # pragma: no cover +from pact_test.clients.http_client import execute_interaction_request def verify_state(interaction, pact_helper, test_instance): + """ + 1. PactHelper.setup() per far partire il servizio + 2. state() per preparare il sistema + 3. creare richiesta basata su Pact + 4. eseguire richiesta + 5. verificare risposta + 6. PactHelper.tear_down() per fermare il servizio + """ pact_helper.setup() - request = _create_request(pact_helper.test_url, pact_helper.test_port, interaction['request']) - if type(request) is Right: - response = _parse_response(request.value) + # EXECUTE state() HERE + response = execute_interaction_request(pact_helper.test_url, pact_helper.test_port, interaction) + # VERIFY response HERE pact_helper.tear_down() return Right(_build_test_result()) -def _verify_request(expected_request, request): - pass - - -def _create_request(url, port, request_body): - request_port = '80' if port is None else str(port) - request_url = 'http://' + url + ':' + request_port - - request = Request(request_url) - request.method = request_body['method'] - request.selector = request_body['path'] + request_body['query'] - request_headers = request_body.get('headers', {}) - for header in request_headers: - request.add_header(header, request_headers[header]) - return Right(request) - - -def _parse_response(request): - try: - response = urlopen(request) - response_body = response.read() - response_type = _get_response_type(response).value - if 'application/json' in response_type: - return Right(json.loads(response_body)) - return Right(str(response_body.decode('utf-8'))) - except Exception as e: - return Left(str(e)) - - -def _get_response_type(response): # pragma: no cover - try: # pragma: no cover - return Right(response.getheader('Content-type')) # pragma: no cover - except: # pragma: no cover - return Right(response.headers['Content-Type']) # pragma: no cover - - def _build_test_result(status='PASSED', reason=None): return {'status': status, 'reason': reason} diff --git a/tests/runners/service_consumers/state_test.py b/tests/runners/service_consumers/state_test.py index f4065db..7db08dc 100644 --- a/tests/runners/service_consumers/state_test.py +++ b/tests/runners/service_consumers/state_test.py @@ -1,9 +1,8 @@ +import requests from pact_test import state from pact_test import PactHelper from pact_test import ServiceConsumerTest from pact_test.runners.service_consumers.state_test import verify_state -from pact_test.runners.service_consumers.state_test import _create_request -from pact_test.runners.service_consumers.state_test import _parse_response class MyPactHelper(PactHelper): @@ -24,6 +23,15 @@ def test_get_book(self): def test_verify_state(mocker): + class Response(object): + status_code = 200 + headers = {'Content-Type': 'application/json'} + + def json(self): + return {'spam': 'eggs'} + + mocker.patch.object(requests, 'request', lambda x, **kwargs: Response()) + test_instance = TestLibraryApp() pact_helper = MyPactHelper() From e6017c3d0d984256809ca853713815a9346048fc Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Mon, 12 Jun 2017 16:43:10 +1000 Subject: [PATCH 27/85] Response matcher. --- pact_test/matchers/response_matcher.py | 50 +++++++++++++++++++++++ tests/matchers/response_matcher.py | 56 ++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 pact_test/matchers/response_matcher.py create mode 100644 tests/matchers/response_matcher.py diff --git a/pact_test/matchers/response_matcher.py b/pact_test/matchers/response_matcher.py new file mode 100644 index 0000000..829680e --- /dev/null +++ b/pact_test/matchers/response_matcher.py @@ -0,0 +1,50 @@ +from pact_test.either import * + + +def match(interaction, pact_response): + return _match_status(interaction, pact_response)\ + .concat(_match_headers, pact_response)\ + .concat(_match_body, pact_response) + + +def _match_status(interaction, pact_response): + expected = interaction['response'].get('status') + actual = pact_response.status + + if actual == expected: + return Right(interaction) + return Left(_build_error_message('status', expected, actual)) + + +def _match_headers(interaction, pact_response): + expected = interaction['response'].get('headers') + actual = _to_dict(pact_response.headers) + + if _is_subset(expected, actual): + return Right(interaction) + return Left(_build_error_message('headers', expected, actual)) + + +def _match_body(interaction, pact_response): + expected = interaction['response'].get('body') + actual = pact_response.body + + if _is_subset(expected, actual): + return Right(interaction) + return Left(_build_error_message('body', expected, actual)) + + +def _to_dict(headers): + d = {} + for h in headers: + d[h[0]] = h[1] + return d + + +def _build_error_message(section, expected, actual): + return 'Non-matching ' + section + ' for the response. Expected:\n\n\t' + \ + str(expected) + '\n\nReceived:\n\n\t' + str(actual) + + +def _is_subset(expected, actual): + return all(item in expected.items() for item in actual.items()) diff --git a/tests/matchers/response_matcher.py b/tests/matchers/response_matcher.py new file mode 100644 index 0000000..a3d1999 --- /dev/null +++ b/tests/matchers/response_matcher.py @@ -0,0 +1,56 @@ +from pact_test.either import Right +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match + + +interaction = { + 'response': { + 'status': 418, + 'headers': {'Content-Type': 'spam'}, + 'body': {'spam': 'eggs'} + } +} + + +def valid_response(): + return PactResponse( + status=418, + headers=[('Content-Type', 'spam')], + body={'spam': 'eggs'} + ) + + +def test_non_matching_status(): + pact_response = PactResponse(status=200) + msg = 'Non-matching status for the response. Expected:\n\n\t' + \ + str(418) + '\n\nReceived:\n\n\t' + str(200) + + assert match(interaction, pact_response).value == msg + + +def test_non_matching_headers(): + pact_response = PactResponse(status=418, headers=[('Date', '12-06-2017')]) + msg = 'Non-matching headers for the response. Expected:\n\n\t' + \ + str({'Content-Type': 'spam'}) + '\n\nReceived:\n\n\t' + \ + str({'Date': '12-06-2017'}) + + assert match(interaction, pact_response).value == msg + + +def test_non_matching_body(): + pact_response = PactResponse( + status=418, + headers=[('Content-Type', 'spam')], + body={'spam': 'spam'} + ) + msg = 'Non-matching body for the response. Expected:\n\n\t' + \ + str({'spam': 'eggs'}) + '\n\nReceived:\n\n\t' + \ + str({'spam': 'spam'}) + print(match(interaction, pact_response).value) + assert match(interaction, pact_response).value == msg + + +def test_matching_response(): + pact_response = valid_response() + + assert type(match(interaction, pact_response)) is Right From 74250e2350fa1f9d50191a85cdb43160bf6e5986 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Mon, 12 Jun 2017 17:07:34 +1000 Subject: [PATCH 28/85] WIP --- pact_test/runners/service_consumers/state_test.py | 9 +++------ tests/runners/service_consumers/state_test.py | 4 +--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/pact_test/runners/service_consumers/state_test.py b/pact_test/runners/service_consumers/state_test.py index 6c22059..417c43f 100644 --- a/pact_test/runners/service_consumers/state_test.py +++ b/pact_test/runners/service_consumers/state_test.py @@ -1,4 +1,4 @@ -from pact_test.either import * +from pact_test.matchers.response_matcher import match from pact_test.clients.http_client import execute_interaction_request @@ -15,9 +15,6 @@ def verify_state(interaction, pact_helper, test_instance): # EXECUTE state() HERE response = execute_interaction_request(pact_helper.test_url, pact_helper.test_port, interaction) # VERIFY response HERE + response_verification = None pact_helper.tear_down() - return Right(_build_test_result()) - - -def _build_test_result(status='PASSED', reason=None): - return {'status': status, 'reason': reason} + return response_verification diff --git a/tests/runners/service_consumers/state_test.py b/tests/runners/service_consumers/state_test.py index 7db08dc..d13eda1 100644 --- a/tests/runners/service_consumers/state_test.py +++ b/tests/runners/service_consumers/state_test.py @@ -38,10 +38,8 @@ def json(self): mocker.spy(pact_helper, 'setup') mocker.spy(pact_helper, 'tear_down') - response = verify_state(interaction, pact_helper, test_instance).value - expected_response = {'status': 'PASSED', 'reason': None} + response = verify_state(interaction, pact_helper, test_instance) - assert response == expected_response assert pact_helper.setup.call_count == 1 assert pact_helper.tear_down.call_count == 1 assert pact_helper.tear_down.call_count == 1 From 8d31d85d4691378e2387b7e3245178b2c2e24fba Mon Sep 17 00:00:00 2001 From: Guido Barbaglia Date: Tue, 13 Jun 2017 15:24:57 +1000 Subject: [PATCH 29/85] Added "SayThanks.io" badges --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index cc9ab97..2f733a8 100644 --- a/README.rst +++ b/README.rst @@ -10,6 +10,8 @@ :target: https://travis-ci.org/Kalimaha/pact-test .. image:: https://coveralls.io/repos/github/Kalimaha/pact-test/badge.svg?branch=development :target: https://coveralls.io/github/Kalimaha/pact-test?branch=development +.. image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg + :target: https://saythanks.io/to/Kalimaha Pact Test for Python =============== From 9556301be5ad61037775d82b26aecfbcd5ddb45e Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 17 Jun 2017 10:10:15 +1000 Subject: [PATCH 30/85] Use Requets to fetch Pact. --- pact_test/utils/pact_utils.py | 22 +++++++-------- tests/utils/pact_utils.py | 50 +++++++++++++++-------------------- 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/pact_test/utils/pact_utils.py b/pact_test/utils/pact_utils.py index ced621e..12df1ea 100644 --- a/pact_test/utils/pact_utils.py +++ b/pact_test/utils/pact_utils.py @@ -1,8 +1,6 @@ import json -try: # pragma: no cover - from urllib.request import urlopen # pragma: no cover -except ImportError: # pragma: no cover - from urllib import urlopen # pragma: no cover +import requests +from pact_test.either import * def get_pact(location): @@ -12,13 +10,15 @@ def get_pact(location): def __get_pact_from_file(filename): - with open(filename) as file_content: - return json.loads(file_content.read()) + try: + with open(filename) as file_content: + return Right(json.loads(file_content.read())) + except Exception as e: + return Left(str(e)) def __get_pact_from_url(url): - return json.loads(__url_content(url)) - - -def __url_content(url): # pragma: no cover - return urlopen(url).read() # pragma: no cover + try: + return Right(requests.get(url).json()) + except Exception as e: + return Left(str(e)) diff --git a/tests/utils/pact_utils.py b/tests/utils/pact_utils.py index 249e074..59e1861 100644 --- a/tests/utils/pact_utils.py +++ b/tests/utils/pact_utils.py @@ -1,20 +1,28 @@ import os -try: - from urllib.request import urlopen -except ImportError: - from urllib import urlopen -from pact_test.utils import pact_utils +import requests +from pact_test.either import Left +from pact_test.utils.pact_utils import get_pact -def test_get_pact_from_url(monkeypatch): - def fake_website(_): - return '{"spam": "eggs"}' - monkeypatch.setattr(pact_utils, '__url_content', fake_website) +def test_get_pact_from_url(mocker): + class FakeResponse(object): + def json(self): + return {'spam': 'eggs'} + + mocker.patch.object(requests, 'get') + requests.get.return_value = FakeResponse() url = 'http://montyphyton.com/' url_content = {'spam': 'eggs'} + assert get_pact(url).value == url_content + - assert pact_utils.__get_pact_from_url(url) == url_content +def test_get_pact_from_url_with_errors(mocker): + def bad_url(_): + raise Exception('Boom!') + mocker.patch.object(requests, 'get', new=bad_url) + + assert get_pact('http://montyphyton.com/').value == 'Boom!' def test_get_pact_from_file(): @@ -22,23 +30,9 @@ def test_get_pact_from_file(): 'pact_files', 'file.json') file_content = {'spam': 'eggs'} - assert pact_utils.__get_pact_from_file(filename) == file_content - + assert get_pact(filename).value == file_content -def test_generic_get_pact_from_url(monkeypatch): - def fake_website(_): - return '{"spam": "eggs"}' - monkeypatch.setattr(pact_utils, '__url_content', fake_website) - - url = 'http://montyphyton.com/' - url_content = {'spam': 'eggs'} - - assert pact_utils.get_pact(url) == url_content - - -def test_generic_get_pact_from_file(): - filename = os.path.join(os.getcwd(), 'tests', 'resources', - 'pact_files', 'file.json') - file_content = {'spam': 'eggs'} - assert pact_utils.get_pact(filename) == file_content +def test_get_pact_from_file_with_errors(): + filename = os.path.join(os.getcwd()) + assert type(get_pact(filename)) is Left From 99b38f6199305ba3602bc07577e1ff5962e154ee Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 17 Jun 2017 11:18:06 +1000 Subject: [PATCH 31/85] First consumer test. --- pact_test/clients/http_client.py | 2 +- pact_test/constants/__init__.py | 3 ++ pact_test/matchers/response_matcher.py | 5 +- .../runners/service_consumers/state_test.py | 48 +++++++++++++------ .../runners/service_consumers/test_suite.py | 1 + tests/runners/service_consumers/state_test.py | 42 +++++++++++++++- 6 files changed, 82 insertions(+), 19 deletions(-) diff --git a/pact_test/clients/http_client.py b/pact_test/clients/http_client.py index 918df4d..5647f95 100644 --- a/pact_test/clients/http_client.py +++ b/pact_test/clients/http_client.py @@ -29,7 +29,7 @@ def _parse_body(server_response, content_type): def _parse_headers(server_response): headers = [] - server_headers = server_response.headers + server_headers = server_response.headers or [] for key in server_headers: headers.append((key, server_headers[key])) return headers diff --git a/pact_test/constants/__init__.py b/pact_test/constants/__init__.py index 6fa28c2..6f140c4 100644 --- a/pact_test/constants/__init__.py +++ b/pact_test/constants/__init__.py @@ -1,3 +1,6 @@ +PASSED = 'PASSED' +FAILED = 'FAILED' +PROVIDER_STATE = 'providerState' MISSING_STATE = 'Missing implementation for state ' MISSING_PACT_HELPER = 'Missing "pact_helper.py" at "' MISSING_PACT_URI = 'Missing setup for "pact_uri" at ' diff --git a/pact_test/matchers/response_matcher.py b/pact_test/matchers/response_matcher.py index 829680e..c0b656b 100644 --- a/pact_test/matchers/response_matcher.py +++ b/pact_test/matchers/response_matcher.py @@ -47,4 +47,7 @@ def _build_error_message(section, expected, actual): def _is_subset(expected, actual): - return all(item in expected.items() for item in actual.items()) + actual_items = actual.items() if actual else {} + expected_items = expected.items() if expected else {} + + return all(item in expected_items for item in actual_items) diff --git a/pact_test/runners/service_consumers/state_test.py b/pact_test/runners/service_consumers/state_test.py index 417c43f..f0ef416 100644 --- a/pact_test/runners/service_consumers/state_test.py +++ b/pact_test/runners/service_consumers/state_test.py @@ -1,20 +1,38 @@ from pact_test.matchers.response_matcher import match +from pact_test.either import * +from pact_test.constants import * from pact_test.clients.http_client import execute_interaction_request def verify_state(interaction, pact_helper, test_instance): - """ - 1. PactHelper.setup() per far partire il servizio - 2. state() per preparare il sistema - 3. creare richiesta basata su Pact - 4. eseguire richiesta - 5. verificare risposta - 6. PactHelper.tear_down() per fermare il servizio - """ - pact_helper.setup() - # EXECUTE state() HERE - response = execute_interaction_request(pact_helper.test_url, pact_helper.test_port, interaction) - # VERIFY response HERE - response_verification = None - pact_helper.tear_down() - return response_verification + state = find_state(interaction, test_instance) + if type(state) is Right: + pact_helper.setup() + state.value() + response = execute_interaction_request(pact_helper.test_url, pact_helper.test_port, interaction) + response_verification = match(interaction, response) + output = _build_state_response(state, response_verification) + pact_helper.tear_down() + return output + return state + + +def _build_state_response(state, response_verification): + if type(response_verification) is Right: + return Right(_format_message(state.value.state, PASSED, [])) + else: + errors = [response_verification.value] + return Left(_format_message(state.value.state, FAILED, errors)) + + +def find_state(interaction, test_instance): + state = interaction[PROVIDER_STATE] + for s in test_instance.states: + if s.state == state: + return Right(s) + message = 'Missing state implementation for "' + state + '"' + return Left(_format_message(state, FAILED, [message])) + + +def _format_message(state, status, errors): + return {'state': state, 'status': status, 'errors': errors} diff --git a/pact_test/runners/service_consumers/test_suite.py b/pact_test/runners/service_consumers/test_suite.py index d9b555c..fe3e68b 100644 --- a/pact_test/runners/service_consumers/test_suite.py +++ b/pact_test/runners/service_consumers/test_suite.py @@ -27,6 +27,7 @@ def verify_test(self, test): validity_check = test.is_valid() if type(validity_check) is Right: pact = self.get_pact(test.pact_uri) + print(pact) pact_states = list(map(lambda i: i['providerState'], pact['interactions'])) test_states = list(map(lambda s: s.state, test.states)) diff --git a/tests/runners/service_consumers/state_test.py b/tests/runners/service_consumers/state_test.py index d13eda1..b57f06f 100644 --- a/tests/runners/service_consumers/state_test.py +++ b/tests/runners/service_consumers/state_test.py @@ -2,6 +2,7 @@ from pact_test import state from pact_test import PactHelper from pact_test import ServiceConsumerTest +from pact_test.runners.service_consumers.state_test import find_state from pact_test.runners.service_consumers.state_test import verify_state @@ -22,13 +23,37 @@ def test_get_book(self): pact_helper = MyPactHelper() +def test_find_state(): + response = find_state(interaction, test_instance).value + assert type(response).__name__.endswith('method') + + +def test_find_state_missing(): + class BadTest(ServiceConsumerTest): + pass + + bad_test_instance = BadTest() + expected_response = { + 'state': 'some books exist', + 'status': 'FAILED', + 'errors': [ + 'Missing state implementation for "some books exist"' + ] + } + response = find_state(interaction, bad_test_instance).value + assert response == expected_response + + def test_verify_state(mocker): class Response(object): status_code = 200 headers = {'Content-Type': 'application/json'} def json(self): - return {'spam': 'eggs'} + return { + 'id': 42, + 'title': 'The Hitchhicker\'s Guide to the Galaxy' + } mocker.patch.object(requests, 'request', lambda x, **kwargs: Response()) @@ -38,11 +63,17 @@ def json(self): mocker.spy(pact_helper, 'setup') mocker.spy(pact_helper, 'tear_down') - response = verify_state(interaction, pact_helper, test_instance) + response = verify_state(interaction, pact_helper, test_instance).value + expected_response = { + 'state': 'some books exist', + 'status': 'PASSED', + 'errors': [] + } assert pact_helper.setup.call_count == 1 assert pact_helper.tear_down.call_count == 1 assert pact_helper.tear_down.call_count == 1 + assert response == expected_response interaction = { @@ -63,6 +94,9 @@ def json(self): 'body': { 'id': 42, 'title': 'The Hitchhicker\'s Guide to the Galaxy' + }, + 'headers': { + 'Content-Type': 'application/json' } } } @@ -72,3 +106,7 @@ class TestLibraryApp(ServiceConsumerTest): @state('some books exist') def test_get_book(self): pass + + @state('no books exist') + def test_no_book(self): + pass From 8988d4db0f3f30cf6c77d0fd85f0473c46386e7d Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 17 Jun 2017 15:42:07 +1000 Subject: [PATCH 32/85] First end-to-end test. --- pact_test/clients/http_client.py | 40 +-- pact_test/constants/__init__.py | 3 + .../runners/service_consumers/state_test.py | 13 +- .../runners/service_consumers/test_suite.py | 49 +--- pact_test/utils/pact_helper_utils.py | 4 +- tests/clients/http_client.py | 15 +- tests/matchers/response_matcher.py | 1 - .../service_consumers/pact_helper.py | 9 + .../test_restaurant_customer.py | 2 +- .../service_consumers_bad_pact/pact_helper.py | 9 + .../test_restaurant_customer.py | 10 + tests/runners/service_consumers/state_test.py | 18 +- tests/runners/service_consumers/test_suite.py | 257 +++++++++--------- 13 files changed, 243 insertions(+), 187 deletions(-) create mode 100644 tests/resources/service_consumers_bad_pact/pact_helper.py create mode 100644 tests/resources/service_consumers_bad_pact/test_restaurant_customer.py diff --git a/pact_test/clients/http_client.py b/pact_test/clients/http_client.py index 5647f95..6b6e630 100644 --- a/pact_test/clients/http_client.py +++ b/pact_test/clients/http_client.py @@ -1,23 +1,31 @@ import requests +from pact_test.either import * +from pact_test.constants import * from pact_test.models.response import PactResponse -TEXT = 'text' -JSON = 'application/json' - - def execute_interaction_request(url, port, interaction): url = _build_url(url, port, interaction) + method = interaction[REQUEST].get('method', 'GET') + server_response = _server_response(method, url=url) + + if type(server_response) is Right: + headers = _parse_headers(server_response.value) + content_type = _get_content_type(headers) + return Right(PactResponse( + status=server_response.value.status_code, + headers=headers, + body=_parse_body(server_response.value, content_type) + )) - server_response = requests.request('GET', url=url) - headers = _parse_headers(server_response) - content_type = _get_content_type(headers) + return server_response - return PactResponse( - status=server_response.status_code, - headers=headers, - body=_parse_body(server_response, content_type) - ) + +def _server_response(method, url): + try: + return Right(requests.request(method, url=url)) + except Exception as e: + return Left(str(e)) def _parse_body(server_response, content_type): @@ -36,13 +44,11 @@ def _parse_headers(server_response): def _build_url(url, port, interaction): - path = interaction['request'].get('path', '') - query = interaction['request'].get('query', '') + path = interaction[REQUEST].get('path', '') + query = interaction[REQUEST].get('query', '') test_port = str(port) if port else str(80) - test_url = 'http://' + url + ':' + test_port + path + query - - return test_url + return 'http://' + url + ':' + test_port + path + query def _get_content_type(headers): diff --git a/pact_test/constants/__init__.py b/pact_test/constants/__init__.py index 6f140c4..5e7c6ce 100644 --- a/pact_test/constants/__init__.py +++ b/pact_test/constants/__init__.py @@ -1,3 +1,6 @@ +REQUEST = 'request' +TEXT = 'text' +JSON = 'application/json' PASSED = 'PASSED' FAILED = 'FAILED' PROVIDER_STATE = 'providerState' diff --git a/pact_test/runners/service_consumers/state_test.py b/pact_test/runners/service_consumers/state_test.py index f0ef416..50631f3 100644 --- a/pact_test/runners/service_consumers/state_test.py +++ b/pact_test/runners/service_consumers/state_test.py @@ -9,14 +9,21 @@ def verify_state(interaction, pact_helper, test_instance): if type(state) is Right: pact_helper.setup() state.value() - response = execute_interaction_request(pact_helper.test_url, pact_helper.test_port, interaction) - response_verification = match(interaction, response) - output = _build_state_response(state, response_verification) + output = _execute_request(pact_helper, interaction) + if type(output) is Right: + response_verification = match(interaction, output.value) + output = _build_state_response(state, response_verification) pact_helper.tear_down() return output return state +def _execute_request(pact_helper, interaction): + url = pact_helper.test_url + port = pact_helper.test_port + return execute_interaction_request(url, port, interaction) + + def _build_state_response(state, response_verification): if type(response_verification) is Right: return Right(_format_message(state.value.state, PASSED, [])) diff --git a/pact_test/runners/service_consumers/test_suite.py b/pact_test/runners/service_consumers/test_suite.py index fe3e68b..376dbf8 100644 --- a/pact_test/runners/service_consumers/test_suite.py +++ b/pact_test/runners/service_consumers/test_suite.py @@ -2,9 +2,10 @@ import imp import inspect from pact_test.either import * -from pact_test.utils.pact_helper_utils import load_pact_helper from pact_test.constants import * from pact_test.utils.pact_utils import get_pact +from pact_test.utils.pact_helper_utils import load_pact_helper +from pact_test.runners.service_consumers.state_test import verify_state class ServiceConsumerTestSuiteRunner(object): @@ -16,45 +17,21 @@ def __init__(self, config): def verify(self): pact_helper = load_pact_helper(self.config.consumer_tests_path) if type(pact_helper) is Right: - self.pact_helper = pact_helper - return self.collect_tests() >> self.verify_tests + self.pact_helper = pact_helper.value + tests = self.collect_tests().value + return list(map(self.verify_test, tests)) return pact_helper - def verify_tests(self, tests): - return list(map(self.verify_test, tests)) - def verify_test(self, test): validity_check = test.is_valid() if type(validity_check) is Right: - pact = self.get_pact(test.pact_uri) - print(pact) - - pact_states = list(map(lambda i: i['providerState'], pact['interactions'])) - test_states = list(map(lambda s: s.state, test.states)) - - for pact_state in pact_states: - if pact_state not in test_states: - msg = MISSING_STATE + '"' + pact_state + '".' - return Left(msg) - self.verify_state(test.states, pact_state) - else: - return validity_check - - def verify_state(self, states, provider_state): - # for s in states: - # print('\t' + s.state) - # print(providerState) - # print('PACT HELPER SETUP') - # print('EXECUTE STATE') - # print('CREATE REQUEST') - # print('EXECUTE REQUEST') - # print('VERIFY RESPONSE') - # print('PACT HELPER TEAR DOWN') - pass - - @staticmethod - def get_pact(location): # pragma: no cover - return get_pact(location) # pragma: no cover + pact = get_pact(test.pact_uri) + if type(pact) is Right: + interactions = pact.value.get('interactions', {}) + test_results = [verify_state(i, self.pact_helper, test) for i in interactions] + return {'test': test.__class__.__name__, 'results': test_results} + return pact + return validity_check def collect_tests(self): root = self.config.consumer_tests_path @@ -67,7 +44,7 @@ def collect_tests(self): if inspect.isclass(obj) and len(inspect.getmro(obj)) > 2: test_parent = inspect.getmro(obj)[1].__name__ if test_parent == 'ServiceConsumerTest': - tests.append(obj) + tests.append(obj()) if not files: return Left(MISSING_TESTS) diff --git a/pact_test/utils/pact_helper_utils.py b/pact_test/utils/pact_helper_utils.py index a60bc97..9d4aea4 100644 --- a/pact_test/utils/pact_helper_utils.py +++ b/pact_test/utils/pact_helper_utils.py @@ -9,7 +9,9 @@ def load_pact_helper(consumer_tests_path): - return _path_to_pact_helper(consumer_tests_path).concat(_load_module, 'pact_helper') >> _load_user_class + return _path_to_pact_helper(consumer_tests_path)\ + .concat(_load_module, 'pact_helper') \ + >> _load_user_class def _load_user_class(user_module): diff --git a/tests/clients/http_client.py b/tests/clients/http_client.py index 385eb32..12b2d18 100644 --- a/tests/clients/http_client.py +++ b/tests/clients/http_client.py @@ -4,6 +4,17 @@ from pact_test.clients.http_client import execute_interaction_request +def test_http_error(mocker): + def boom(_, **kwargs): + raise Exception('Boom!') + + mocker.patch.object(requests, 'request', new=boom) + + interaction = {'request': {'path': '/spam/eggs'}} + response = execute_interaction_request('', None, interaction).value + assert response == 'Boom!' + + def test_execute_interaction_request(mocker): class Response(object): status_code = 200 @@ -16,7 +27,7 @@ def json(self): url = 'montypython.com' port = None interaction = {'request': {'path': '/spam/eggs'}} - response = execute_interaction_request(url, port, interaction) + response = execute_interaction_request(url, port, interaction).value assert response.status == 200 assert len(response.headers) > 1 @@ -35,7 +46,7 @@ def text(self): url = 'montypython.com' port = None interaction = {'request': {'path': '/spam/eggs'}} - response = execute_interaction_request(url, port, interaction) + response = execute_interaction_request(url, port, interaction).value assert response.status == 200 assert response.headers == [('Date', '12-06-2017')] diff --git a/tests/matchers/response_matcher.py b/tests/matchers/response_matcher.py index a3d1999..123f9f6 100644 --- a/tests/matchers/response_matcher.py +++ b/tests/matchers/response_matcher.py @@ -46,7 +46,6 @@ def test_non_matching_body(): msg = 'Non-matching body for the response. Expected:\n\n\t' + \ str({'spam': 'eggs'}) + '\n\nReceived:\n\n\t' + \ str({'spam': 'spam'}) - print(match(interaction, pact_response).value) assert match(interaction, pact_response).value == msg diff --git a/tests/resources/service_consumers/pact_helper.py b/tests/resources/service_consumers/pact_helper.py index e69de29..06f61cd 100644 --- a/tests/resources/service_consumers/pact_helper.py +++ b/tests/resources/service_consumers/pact_helper.py @@ -0,0 +1,9 @@ +from pact_test import PactHelper + + +class MyPactHelper(PactHelper): + def setup(self): + pass + + def tear_down(self): + pass diff --git a/tests/resources/service_consumers/test_restaurant_customer.py b/tests/resources/service_consumers/test_restaurant_customer.py index ecd38ce..c09c14f 100644 --- a/tests/resources/service_consumers/test_restaurant_customer.py +++ b/tests/resources/service_consumers/test_restaurant_customer.py @@ -2,7 +2,7 @@ @has_pact_with('Restaurant') -@pact_uri('http://google.com/') +@pact_uri('tests/resources/pact_files/simple.json') class TestRestaurantCustomer(ServiceConsumerTest): @state('the breakfast is available') diff --git a/tests/resources/service_consumers_bad_pact/pact_helper.py b/tests/resources/service_consumers_bad_pact/pact_helper.py new file mode 100644 index 0000000..06f61cd --- /dev/null +++ b/tests/resources/service_consumers_bad_pact/pact_helper.py @@ -0,0 +1,9 @@ +from pact_test import PactHelper + + +class MyPactHelper(PactHelper): + def setup(self): + pass + + def tear_down(self): + pass diff --git a/tests/resources/service_consumers_bad_pact/test_restaurant_customer.py b/tests/resources/service_consumers_bad_pact/test_restaurant_customer.py new file mode 100644 index 0000000..ecd38ce --- /dev/null +++ b/tests/resources/service_consumers_bad_pact/test_restaurant_customer.py @@ -0,0 +1,10 @@ +from pact_test.models.service_consumer_test import * + + +@has_pact_with('Restaurant') +@pact_uri('http://google.com/') +class TestRestaurantCustomer(ServiceConsumerTest): + + @state('the breakfast is available') + def test_get_breakfast(self): + return 'Spam & Eggs' diff --git a/tests/runners/service_consumers/state_test.py b/tests/runners/service_consumers/state_test.py index b57f06f..e388c8f 100644 --- a/tests/runners/service_consumers/state_test.py +++ b/tests/runners/service_consumers/state_test.py @@ -28,6 +28,23 @@ def test_find_state(): assert type(response).__name__.endswith('method') +def test_missing_state(): + i = interaction.copy() + i['providerState'] = 'Catch me if you can' + + test_instance = TestLibraryApp() + pact_helper = MyPactHelper() + + response = verify_state(i, pact_helper, test_instance).value + expected_response = { + 'state': 'Catch me if you can', + 'status': 'FAILED', + 'errors': ['Missing state implementation for "Catch me if you can"'] + } + + assert response == expected_response + + def test_find_state_missing(): class BadTest(ServiceConsumerTest): pass @@ -72,7 +89,6 @@ def json(self): assert pact_helper.setup.call_count == 1 assert pact_helper.tear_down.call_count == 1 - assert pact_helper.tear_down.call_count == 1 assert response == expected_response diff --git a/tests/runners/service_consumers/test_suite.py b/tests/runners/service_consumers/test_suite.py index e1c48aa..ad4ae9e 100644 --- a/tests/runners/service_consumers/test_suite.py +++ b/tests/runners/service_consumers/test_suite.py @@ -1,125 +1,132 @@ -# import os -# import sys -# import imp -# import json -# from pact_test.config.config_builder import Config -# from pact_test.runners.service_consumers.test_suite import ServiceConsumerTestSuiteRunner # nopep8 -# -# -# def test_missing_pact_helper(): -# config = Config() -# t = ServiceConsumerTestSuiteRunner(config) -# msg = 'Missing "pact_helper.py" at "tests/service_consumers".' -# assert t.path_to_pact_helper().value == msg -# -# -# def test_missing_setup_method(): -# remove_pact_helper() -# -# config = Config() -# test_pact_helper_path = os.path.join(os.getcwd(), 'tests', -# 'resources', 'pact_helper_no_setup') -# config.consumer_tests_path = test_pact_helper_path -# t = ServiceConsumerTestSuiteRunner(config) -# msg = 'Missing "setup" method in "pact_helper.py".' -# assert t.load_pact_helper().value == msg -# -# -# def test_missing_tear_down_method(): -# remove_pact_helper() -# -# config = Config() -# test_pact_helper_path = os.path.join(os.getcwd(), 'tests', 'resources', -# 'pact_helper_no_tear_down') -# config.consumer_tests_path = test_pact_helper_path -# t = ServiceConsumerTestSuiteRunner(config) -# msg = 'Missing "tear_down" method in "pact_helper.py".' -# assert t.load_pact_helper().value == msg -# -# -# def test_empty_tests_list(monkeypatch): -# config = Config() -# test_pact_helper_path = os.path.join(os.getcwd(), 'tests', 'resources', -# 'service_consumers') -# config.consumer_tests_path = test_pact_helper_path -# -# def empty_list(_): -# return [] -# monkeypatch.setattr(os, 'listdir', empty_list) -# -# t = ServiceConsumerTestSuiteRunner(config) -# msg = 'There are no consumer tests to verify.' -# assert t.collect_tests().value == msg -# -# -# def test_collect_tests(): -# config = Config() -# test_pact_helper_path = os.path.join(os.getcwd(), 'tests', 'resources', -# 'service_consumers') -# config.consumer_tests_path = test_pact_helper_path -# t = ServiceConsumerTestSuiteRunner(config) -# -# tests = t.collect_tests().value -# assert len(tests) == 1 -# -# test = tests[0]() -# assert test.pact_uri == 'http://google.com/' -# assert test.has_pact_with == 'Restaurant' -# -# state = next(test.states) -# assert state.state == 'the breakfast is available' -# assert state() == 'Spam & Eggs' -# -# -# def test_invalid_test(): -# path = os.path.join(os.getcwd(), 'tests', 'resources', -# 'invalid_service_consumer', 'customer.py') -# module = imp.load_source('invalid_test', path) -# test = module.TestRestaurantCustomer() -# -# t = ServiceConsumerTestSuiteRunner(None) -# msg = 'Missing setup for "has_pact_with"' -# assert t.verify_test(test).value.startswith(msg) -# -# -# def test_verify_missing_state(mocker): -# def pact_content(_): -# s = '{"interactions": [{"providerState": "My State"}]}' -# return json.loads(s) -# -# path = os.path.join(os.getcwd(), 'tests', 'resources', -# 'service_consumers', 'test_restaurant_customer.py') -# module = imp.load_source('consumer_test', path) -# test = module.TestRestaurantCustomer() -# -# t = ServiceConsumerTestSuiteRunner(None) -# mocker.patch.object(t, 'get_pact', new=pact_content) -# -# msg = 'Missing implementation for state "My State".' -# assert t.verify_test(test).value == msg -# -# -# def test_verify_existing_state(mocker): -# def pact_content(_): -# s = '{"interactions": [{"providerState": ' \ -# '"the breakfast is available"}]}' -# return json.loads(s) -# -# path = os.path.join(os.getcwd(), 'tests', 'resources', -# 'service_consumers', 'test_restaurant_customer.py') -# module = imp.load_source('consumer_test', path) -# test = module.TestRestaurantCustomer() -# -# t = ServiceConsumerTestSuiteRunner(None) -# mocker.patch.object(t, 'get_pact', new=pact_content) -# mocker.spy(t, 'verify_state') -# -# t.verify_test(test) -# assert t.verify_state.call_count == 1 -# -# -# def remove_pact_helper(): -# try: -# del sys.modules['pact_helper'] -# except KeyError: -# pass +import os +import sys +import imp +import requests +from pact_test.either import Left +from pact_test.config.config_builder import Config +from pact_test.runners.service_consumers.test_suite import ServiceConsumerTestSuiteRunner # nopep8 + + +def test_verify(monkeypatch): + config = Config() + config.consumer_tests_path = os.path.join(os.getcwd(), 'tests', + 'resources', + 'service_consumers') + sct = ServiceConsumerTestSuiteRunner(config) + + class Response(object): + status_code = 200 + headers = {'Content-Type': 'application/json', 'Date': '12-06-2017'} + + def json(self): + return {'spam': 'eggs'} + + def connect(_, **kwargs): + return Response() + monkeypatch.setattr(requests, 'request', connect) + + actual_response = sct.verify() + + assert len(actual_response) == 1 + assert actual_response[0]['test'] == 'TestRestaurantCustomer' + assert len(actual_response[0]['results']) == 1 + + test_outcome = actual_response[0]['results'][0].value + assert test_outcome['state'] == 'the breakfast is available' + assert test_outcome['status'] == 'FAILED' + assert len(test_outcome['errors']) == 1 + assert test_outcome['errors'][0].startswith('Non-matching headers') + + +def test_verify_bad_pact(): + config = Config() + config.consumer_tests_path = os.path.join(os.getcwd(), 'tests', + 'resources', + 'service_consumers_bad_pact') + sct = ServiceConsumerTestSuiteRunner(config) + actual_response = sct.verify() + assert type(actual_response[0]) is Left + + +def test_missing_pact_helper(): + config = Config() + t = ServiceConsumerTestSuiteRunner(config) + msg = 'Missing "pact_helper.py" at "tests/service_consumers".' + + assert t.verify().value == msg + + +def test_missing_setup_method(): + remove_pact_helper() + + config = Config() + test_pact_helper_path = os.path.join(os.getcwd(), 'tests', + 'resources', 'pact_helper_no_setup') + config.consumer_tests_path = test_pact_helper_path + t = ServiceConsumerTestSuiteRunner(config) + msg = 'Missing "setup" method in "pact_helper.py".' + assert t.verify().value == msg + + +def test_missing_tear_down_method(): + remove_pact_helper() + + config = Config() + test_pact_helper_path = os.path.join(os.getcwd(), 'tests', 'resources', + 'pact_helper_no_tear_down') + config.consumer_tests_path = test_pact_helper_path + t = ServiceConsumerTestSuiteRunner(config) + msg = 'Missing "tear_down" method in "pact_helper.py".' + assert t.verify().value == msg + + +def test_empty_tests_list(monkeypatch): + config = Config() + test_pact_helper_path = os.path.join(os.getcwd(), 'tests', 'resources', + 'service_consumers') + config.consumer_tests_path = test_pact_helper_path + + def empty_list(_): + return [] + monkeypatch.setattr(os, 'listdir', empty_list) + + t = ServiceConsumerTestSuiteRunner(config) + msg = 'There are no consumer tests to verify.' + assert t.collect_tests().value == msg + + +def test_collect_tests(): + config = Config() + test_pact_helper_path = os.path.join(os.getcwd(), 'tests', 'resources', + 'service_consumers') + config.consumer_tests_path = test_pact_helper_path + t = ServiceConsumerTestSuiteRunner(config) + + tests = t.collect_tests().value + assert len(tests) == 1 + + test = tests[0] + assert test.pact_uri == 'tests/resources/pact_files/simple.json' + assert test.has_pact_with == 'Restaurant' + + state = next(test.states) + assert state.state == 'the breakfast is available' + assert state() == 'Spam & Eggs' + + +def test_invalid_test(): + path = os.path.join(os.getcwd(), 'tests', 'resources', + 'invalid_service_consumer', 'customer.py') + module = imp.load_source('invalid_test', path) + test = module.TestRestaurantCustomer() + + t = ServiceConsumerTestSuiteRunner(None) + msg = 'Missing setup for "has_pact_with"' + assert t.verify_test(test).value.startswith(msg) + + +def remove_pact_helper(): + try: + del sys.modules['pact_helper'] + except KeyError: + pass From c2748da4f58032f1fe188c499f12d4ede72c1e41 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Tue, 20 Jun 2017 18:32:18 +1000 Subject: [PATCH 33/85] Use Docker to run tests in multiple Python versions. --- README.rst | 22 ++++++++++++++++++ bin/test | 38 +++++++++++++++++++++++++++++++ tests/environments/Dockerfilepy27 | 7 ++++++ tests/environments/Dockerfilepy33 | 7 ++++++ tests/environments/Dockerfilepy34 | 7 ++++++ tests/environments/Dockerfilepy35 | 7 ++++++ tests/environments/Dockerfilepy36 | 7 ++++++ tox.ini | 2 -- 8 files changed, 95 insertions(+), 2 deletions(-) create mode 100755 bin/test create mode 100644 tests/environments/Dockerfilepy27 create mode 100644 tests/environments/Dockerfilepy33 create mode 100644 tests/environments/Dockerfilepy34 create mode 100644 tests/environments/Dockerfilepy35 create mode 100644 tests/environments/Dockerfilepy36 diff --git a/README.rst b/README.rst index 2f733a8..d73753a 100644 --- a/README.rst +++ b/README.rst @@ -24,3 +24,25 @@ Setup .. code:: bash python setup.py install + +Test +---- + +It is possible to run the tests locally with Docker through the following command: + +.. code:: bash + + $ ./bin/test + +By default this command tests the library against Python 3.6. It is possible to specify the Python version as follows: + +.. code:: bash + + $ ./bin/test + +Available values for `ENV` are: :code:`py27`, :code:`py33`, :code:`py34`, :code:`py35` and :code:`py36`. It is also +possible to test all the versions at once with: + +.. code:: bash + + $ ./bin/test all \ No newline at end of file diff --git a/bin/test b/bin/test new file mode 100755 index 0000000..d5c43e5 --- /dev/null +++ b/bin/test @@ -0,0 +1,38 @@ +#!/bin/bash + + +START=$(date +%s) +ENV=${1:-'py36'} +ENVS=('py27' 'py33' 'py34' 'py35' 'py36') + +run_test () { + echo 'Running tests for '$1'... START' + docker build -t pact-test-$1 -f ./tests/environments/Dockerfile$1 . && \ + docker run pact-test-$1 py.test + echo 'Running tests for '$1'... DONE' +} + +help () { + echo '--------------------------------------------------------' + echo 'Usage: test ' + echo '' + echo 'Available values for : py27, py33, py34, py35, py36' + echo '--------------------------------------------------------' +} + +if [[ "${ENVS[*]}" == *$ENV* ]]; then + run_test $ENV +elif [ "$ENV" == "all" ]; then + for i in "${ENVS[@]}" + do + run_test $i + done +else + help +fi + +END=$(date +%s) +DELTA=$(($END - $START)) + +echo +echo 'Execution completed in' $DELTA 'seconds.' \ No newline at end of file diff --git a/tests/environments/Dockerfilepy27 b/tests/environments/Dockerfilepy27 new file mode 100644 index 0000000..c83f644 --- /dev/null +++ b/tests/environments/Dockerfilepy27 @@ -0,0 +1,7 @@ +FROM python:2.7.13-alpine + +RUN mkdir -p /app +WORKDIR /app +ADD requirements.txt /app +RUN pip install -r requirements.txt +ADD . /app \ No newline at end of file diff --git a/tests/environments/Dockerfilepy33 b/tests/environments/Dockerfilepy33 new file mode 100644 index 0000000..b6262ca --- /dev/null +++ b/tests/environments/Dockerfilepy33 @@ -0,0 +1,7 @@ +FROM python:3.3.6-alpine + +RUN mkdir -p /app +WORKDIR /app +ADD requirements.txt /app +RUN pip install -r requirements.txt +ADD . /app \ No newline at end of file diff --git a/tests/environments/Dockerfilepy34 b/tests/environments/Dockerfilepy34 new file mode 100644 index 0000000..3dd51dc --- /dev/null +++ b/tests/environments/Dockerfilepy34 @@ -0,0 +1,7 @@ +FROM python:3.4.6-alpine + +RUN mkdir -p /app +WORKDIR /app +ADD requirements.txt /app +RUN pip install -r requirements.txt +ADD . /app \ No newline at end of file diff --git a/tests/environments/Dockerfilepy35 b/tests/environments/Dockerfilepy35 new file mode 100644 index 0000000..bd2a2c8 --- /dev/null +++ b/tests/environments/Dockerfilepy35 @@ -0,0 +1,7 @@ +FROM python:3.5.3-alpine + +RUN mkdir -p /app +WORKDIR /app +ADD requirements.txt /app +RUN pip install -r requirements.txt +ADD . /app \ No newline at end of file diff --git a/tests/environments/Dockerfilepy36 b/tests/environments/Dockerfilepy36 new file mode 100644 index 0000000..3a5ee84 --- /dev/null +++ b/tests/environments/Dockerfilepy36 @@ -0,0 +1,7 @@ +FROM python:3.6.1-alpine + +RUN mkdir -p /app +WORKDIR /app +ADD requirements.txt /app +RUN pip install -r requirements.txt +ADD . /app \ No newline at end of file diff --git a/tox.ini b/tox.ini index 2e998f5..9d0ed3e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,5 @@ [tox] -#envlist = py27, py33, py34, py35, py36 envlist = py27, py36 -;envlist = py27 [testenv] deps = From 1a243bd467f1c0f56a005913599e577b9dd83275 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 24 Jun 2017 15:19:53 +1000 Subject: [PATCH 34/85] Return Either of test results. --- pact_test/runners/service_consumers/test_suite.py | 2 +- tests/runners/service_consumers/test_suite.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pact_test/runners/service_consumers/test_suite.py b/pact_test/runners/service_consumers/test_suite.py index 376dbf8..ca5cd09 100644 --- a/pact_test/runners/service_consumers/test_suite.py +++ b/pact_test/runners/service_consumers/test_suite.py @@ -29,7 +29,7 @@ def verify_test(self, test): if type(pact) is Right: interactions = pact.value.get('interactions', {}) test_results = [verify_state(i, self.pact_helper, test) for i in interactions] - return {'test': test.__class__.__name__, 'results': test_results} + return Right({'test': test.__class__.__name__, 'results': test_results}) return pact return validity_check diff --git a/tests/runners/service_consumers/test_suite.py b/tests/runners/service_consumers/test_suite.py index ad4ae9e..685a620 100644 --- a/tests/runners/service_consumers/test_suite.py +++ b/tests/runners/service_consumers/test_suite.py @@ -28,10 +28,10 @@ def connect(_, **kwargs): actual_response = sct.verify() assert len(actual_response) == 1 - assert actual_response[0]['test'] == 'TestRestaurantCustomer' - assert len(actual_response[0]['results']) == 1 + assert actual_response[0].value['test'] == 'TestRestaurantCustomer' + assert len(actual_response[0].value['results']) == 1 - test_outcome = actual_response[0]['results'][0].value + test_outcome = actual_response[0].value['results'][0].value assert test_outcome['state'] == 'the breakfast is available' assert test_outcome['status'] == 'FAILED' assert len(test_outcome['errors']) == 1 From e4e04aac0e47c0339152d79ada9a17a77e240d9e Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 24 Jun 2017 15:39:10 +1000 Subject: [PATCH 35/85] Removed unused files. --- pact_test/matchers/request_matcher.py | 58 ----------------- tests/matchers/request_matcher.py | 90 --------------------------- 2 files changed, 148 deletions(-) delete mode 100644 pact_test/matchers/request_matcher.py delete mode 100644 tests/matchers/request_matcher.py diff --git a/pact_test/matchers/request_matcher.py b/pact_test/matchers/request_matcher.py deleted file mode 100644 index f8d3fbd..0000000 --- a/pact_test/matchers/request_matcher.py +++ /dev/null @@ -1,58 +0,0 @@ -from pact_test.either import * - - -def match(expected, actual): - body = actual.data - method = actual.method - headers = actual.headers - path = actual.selector[0:actual.selector.index('?')] - query = actual.selector[actual.selector.index('?'):] - - return _match_query(expected, query)\ - .concat(_match_path, path)\ - .concat(_match_method, method)\ - .concat(_match_body, body)\ - .concat(_match_headers, headers) - - -def _match_body(expected, actual): - matching_texts = actual == expected['body'] - same_type = type(expected['body']) == type(actual) - matching_dicts = type(actual) is dict and _is_subset(actual, expected['body']) - - if same_type and (matching_dicts or matching_texts): - return Right(expected) - return Left(_build_error_message('body', expected['body'], actual)) - - -def _match_query(expected, actual): - if actual == expected['query']: - return Right(expected) - return Left(_build_error_message('query', expected['query'], actual)) - - -def _match_path(expected, actual): - if actual == expected['path']: - return Right(expected) - return Left(_build_error_message('path', expected['path'], actual)) - - -def _match_method(expected, actual): - if actual == expected['method']: - return Right(expected) - return Left(_build_error_message('method', expected['method'], actual)) - - -def _match_headers(expected, actual): - if _is_subset(actual, expected['headers']): - return Right(expected) - return Left(_build_error_message('headers', expected['headers'], actual)) - - -def _build_error_message(section, expected, actual): - return 'Non-matching ' + section + ' for the request. Expected:\n\n\t' + \ - str(expected) + '\n\nReceived:\n\n\t' + str(actual) - - -def _is_subset(expected, actual): - return all(item in expected.items() for item in actual.items()) diff --git a/tests/matchers/request_matcher.py b/tests/matchers/request_matcher.py deleted file mode 100644 index 0140a71..0000000 --- a/tests/matchers/request_matcher.py +++ /dev/null @@ -1,90 +0,0 @@ -from pact_test.either import * -from pact_test.matchers.request_matcher import match -try: # pragma: no cover - from urllib.request import Request, urlopen # pragma: no cover -except: # pragma: no cover - from urllib2 import Request, urlopen # pragma: no cover - - -expected = { - 'method': 'GET', - 'path': '/books/42', - 'query': '?format=hardcover', - 'headers': {'Content-type': 'application/json'}, - 'body': {'title': 'The Hitchhicker\'s Guide to the Galaxy'} -} - - -def build_request(): - request = Request('http://localhost') - request.method = 'POST' - request.selector = '/movies?format=PAL' - request.add_header('Content', 'silly') - request.data = {'title': 'A Fortune-Teller Told Me'} - return request - - -def test_non_matching_query(): - request = build_request() - msg = 'Non-matching query for the request. Expected:\n\n\t' + \ - str(expected['query']) + '\n\nReceived:\n\n\t' + '?format=PAL' - assert match(expected, request).value == msg - - -def test_non_matching_path(): - request = build_request() - request.selector = '/movies?format=hardcover' - msg = 'Non-matching path for the request. Expected:\n\n\t' + \ - str(expected['path']) + '\n\nReceived:\n\n\t' + '/movies' - assert match(expected, request).value == msg - - -def test_non_matching_method(): - request = build_request() - request.selector = '/books/42?format=hardcover' - msg = 'Non-matching method for the request. Expected:\n\n\t' + \ - str(expected['method']) + '\n\nReceived:\n\n\t' + 'POST' - assert match(expected, request).value == msg - - -def test_non_matching_dict_body(): - request = build_request() - request.selector = '/books/42?format=hardcover' - request.method = 'GET' - msg = 'Non-matching body for the request. Expected:\n\n\t' + \ - str(expected['body']) + '\n\nReceived:\n\n\t' + \ - str({'title': 'A Fortune-Teller Told Me'}) - assert match(expected, request).value == msg - - -def test_non_matching_text_body(): - request = build_request() - request.selector = '/books/42?format=hardcover' - request.method = 'GET' - request.data = 'Spam & Eggs' - expected['body'] = 'Eggs and Bacon' - msg = 'Non-matching body for the request. Expected:\n\n\t' + \ - str(expected['body']) + '\n\nReceived:\n\n\t' + \ - 'Spam & Eggs' - assert match(expected, request).value == msg - - -def test_non_matching_headers(): - request = build_request() - request.selector = '/books/42?format=hardcover' - request.method = 'GET' - request.data = {'title': 'The Hitchhicker\'s Guide to the Galaxy'} - expected['body'] = {'title': 'The Hitchhicker\'s Guide to the Galaxy'} - msg = 'Non-matching headers for the request. Expected:\n\n\t' + \ - str(expected['headers']) + '\n\nReceived:\n\n\t' + \ - str({'Content': 'silly'}) - assert match(expected, request).value == msg - - -def test_matching_request(): - request = build_request() - request.selector = '/books/42?format=hardcover' - request.method = 'GET' - request.data = {'title': 'The Hitchhicker\'s Guide to the Galaxy'} - request.add_header('Content-Type', 'application/json') - assert type(match(expected, request)) is Right From 82cafe19e2cfcb4012203ca80bc995381bad7999 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 24 Jun 2017 16:45:13 +1000 Subject: [PATCH 36/85] Setup for acceptance tests. --- pact_test/matchers/response_matcher.py | 9 +++- tests/acceptance/__init__.py | 0 tests/acceptance/acceptance_test_loader.py | 14 +++++++ .../body/array in different order.json | 26 ++++++++++++ .../body/different value found at index.json | 26 ++++++++++++ .../body/different value found at key.json | 26 ++++++++++++ .../testcases/request/body/matches.json | 30 +++++++++++++ .../testcases/request/body/missing index.json | 26 ++++++++++++ .../testcases/request/body/missing key.json | 27 ++++++++++++ ... null found at key when null expected.json | 26 ++++++++++++ ...ull found in array when null expected.json | 26 ++++++++++++ ... found at key where not null expected.json | 26 ++++++++++++ ...found in array when not null expected.json | 26 ++++++++++++ ...ber found at key when string expected.json | 26 ++++++++++++ ...r found in array when string expected.json | 26 ++++++++++++ .../body/plain text that does not match.json | 18 ++++++++ .../request/body/plain text that matches.json | 18 ++++++++ ...ing found at key when number expected.json | 26 ++++++++++++ ...g found in array when number expected.json | 26 ++++++++++++ .../unexpected index with not null value.json | 26 ++++++++++++ .../unexpected index with null value.json | 26 ++++++++++++ .../unexpected key with not null value.json | 27 ++++++++++++ .../body/unexpected key with null value.json | 27 ++++++++++++ .../request/headers/empty headers.json | 17 ++++++++ .../header name is different case.json | 20 +++++++++ .../header value is different case.json | 20 +++++++++ .../testcases/request/headers/matches.json | 22 ++++++++++ ...mma separated header values different.json | 20 +++++++++ .../headers/unexpected header found.json | 18 ++++++++ .../whitespace after comma different.json | 20 +++++++++ .../request/method/different method.json | 17 ++++++++ .../testcases/request/method/matches.json | 17 ++++++++ .../method/method is different case.json | 17 ++++++++ ...ath found when forward slash expected.json | 18 ++++++++ ... slash found when empty path expected.json | 18 ++++++++ .../request/path/incorrect path.json | 18 ++++++++ .../testcases/request/path/matches.json | 18 ++++++++ .../path/missing trailing slash in path.json | 18 ++++++++ .../unexpected trailing slash in path.json | 18 ++++++++ .../request/query/different param order.json | 18 ++++++++ .../request/query/different param values.json | 18 ++++++++ ...atches with equals in the query value.json | 18 ++++++++ .../testcases/request/query/matches.json | 18 ++++++++ .../request/query/trailing amperand.json | 18 ++++++++ .../body/array_in_different_order.json | 20 +++++++++ .../response/body/deeply_nested_objects.json | 42 +++++++++++++++++++ .../body/different_value_found_at_index.json | 20 +++++++++ .../body/different_value_found_at_key.json | 20 +++++++++ .../body/keys_out_of_order_match.json | 18 ++++++++ .../testcases/response/body/matches.json | 24 +++++++++++ .../response/body/missing_index.json | 20 +++++++++ .../testcases/response/body/missing_key.json | 21 ++++++++++ ..._null_found_at_key_when_null_expected.json | 20 +++++++++ ...ull_found_in_array_when_null_expected.json | 20 +++++++++ ..._found_at_key_where_not_null_expected.json | 20 +++++++++ ...found_in_array_when_not_null_expected.json | 20 +++++++++ ...ber_found_at_key_when_string_expected.json | 20 +++++++++ ...r_found_in_array_when_string_expected.json | 20 +++++++++ .../body/objects_in_array_first_matches.json | 19 +++++++++ .../body/objects_in_array_no_matches.json | 20 +++++++++ .../body/objects_in_array_second_matches.json | 19 +++++++++ .../body/plain_text_that_does_not_match.json | 12 ++++++ .../body/plain_text_that_matches.json | 12 ++++++ .../body/property_name_is_different_case.json | 20 +++++++++ ...ing_found_at_key_when_number_expected.json | 20 +++++++++ ...g_found_in_array_when_number_expected.json | 20 +++++++++ .../unexpected_index_with_not_null_value.json | 20 +++++++++ .../unexpected_index_with_null_value.json | 20 +++++++++ .../unexpected_key_with_not_null_value.json | 21 ++++++++++ .../body/unexpected_key_with_null_value.json | 21 ++++++++++ .../response/headers/empty_headers.json | 11 +++++ .../header_name_is_different_case.json | 14 +++++++ .../header_value_is_different_case.json | 14 +++++++ .../testcases/response/headers/matches.json | 16 +++++++ ...mma_separated_header_values_different.json | 14 +++++++ .../headers/unexpected_header_found.json | 12 ++++++ .../whitespace_after_comma_different.json | 14 +++++++ .../response/status/different_status.json | 10 +++++ .../response/status/different_status.py | 19 +++++++++ .../testcases/response/status/matches.json | 10 +++++ tests/matchers/response_matcher.py | 26 ++++++++---- tests/runners/service_consumers/test_suite.py | 2 +- 82 files changed, 1600 insertions(+), 11 deletions(-) create mode 100644 tests/acceptance/__init__.py create mode 100644 tests/acceptance/acceptance_test_loader.py create mode 100755 tests/acceptance/version_1/testcases/request/body/array in different order.json create mode 100755 tests/acceptance/version_1/testcases/request/body/different value found at index.json create mode 100755 tests/acceptance/version_1/testcases/request/body/different value found at key.json create mode 100755 tests/acceptance/version_1/testcases/request/body/matches.json create mode 100755 tests/acceptance/version_1/testcases/request/body/missing index.json create mode 100755 tests/acceptance/version_1/testcases/request/body/missing key.json create mode 100755 tests/acceptance/version_1/testcases/request/body/not null found at key when null expected.json create mode 100755 tests/acceptance/version_1/testcases/request/body/not null found in array when null expected.json create mode 100755 tests/acceptance/version_1/testcases/request/body/null found at key where not null expected.json create mode 100755 tests/acceptance/version_1/testcases/request/body/null found in array when not null expected.json create mode 100755 tests/acceptance/version_1/testcases/request/body/number found at key when string expected.json create mode 100755 tests/acceptance/version_1/testcases/request/body/number found in array when string expected.json create mode 100755 tests/acceptance/version_1/testcases/request/body/plain text that does not match.json create mode 100755 tests/acceptance/version_1/testcases/request/body/plain text that matches.json create mode 100755 tests/acceptance/version_1/testcases/request/body/string found at key when number expected.json create mode 100755 tests/acceptance/version_1/testcases/request/body/string found in array when number expected.json create mode 100755 tests/acceptance/version_1/testcases/request/body/unexpected index with not null value.json create mode 100755 tests/acceptance/version_1/testcases/request/body/unexpected index with null value.json create mode 100755 tests/acceptance/version_1/testcases/request/body/unexpected key with not null value.json create mode 100755 tests/acceptance/version_1/testcases/request/body/unexpected key with null value.json create mode 100755 tests/acceptance/version_1/testcases/request/headers/empty headers.json create mode 100755 tests/acceptance/version_1/testcases/request/headers/header name is different case.json create mode 100755 tests/acceptance/version_1/testcases/request/headers/header value is different case.json create mode 100755 tests/acceptance/version_1/testcases/request/headers/matches.json create mode 100755 tests/acceptance/version_1/testcases/request/headers/order of comma separated header values different.json create mode 100755 tests/acceptance/version_1/testcases/request/headers/unexpected header found.json create mode 100755 tests/acceptance/version_1/testcases/request/headers/whitespace after comma different.json create mode 100755 tests/acceptance/version_1/testcases/request/method/different method.json create mode 100755 tests/acceptance/version_1/testcases/request/method/matches.json create mode 100755 tests/acceptance/version_1/testcases/request/method/method is different case.json create mode 100755 tests/acceptance/version_1/testcases/request/path/empty path found when forward slash expected.json create mode 100755 tests/acceptance/version_1/testcases/request/path/forward slash found when empty path expected.json create mode 100755 tests/acceptance/version_1/testcases/request/path/incorrect path.json create mode 100755 tests/acceptance/version_1/testcases/request/path/matches.json create mode 100755 tests/acceptance/version_1/testcases/request/path/missing trailing slash in path.json create mode 100755 tests/acceptance/version_1/testcases/request/path/unexpected trailing slash in path.json create mode 100755 tests/acceptance/version_1/testcases/request/query/different param order.json create mode 100755 tests/acceptance/version_1/testcases/request/query/different param values.json create mode 100755 tests/acceptance/version_1/testcases/request/query/matches with equals in the query value.json create mode 100755 tests/acceptance/version_1/testcases/request/query/matches.json create mode 100755 tests/acceptance/version_1/testcases/request/query/trailing amperand.json create mode 100755 tests/acceptance/version_1/testcases/response/body/array_in_different_order.json create mode 100755 tests/acceptance/version_1/testcases/response/body/deeply_nested_objects.json create mode 100755 tests/acceptance/version_1/testcases/response/body/different_value_found_at_index.json create mode 100755 tests/acceptance/version_1/testcases/response/body/different_value_found_at_key.json create mode 100755 tests/acceptance/version_1/testcases/response/body/keys_out_of_order_match.json create mode 100755 tests/acceptance/version_1/testcases/response/body/matches.json create mode 100755 tests/acceptance/version_1/testcases/response/body/missing_index.json create mode 100755 tests/acceptance/version_1/testcases/response/body/missing_key.json create mode 100755 tests/acceptance/version_1/testcases/response/body/not_null_found_at_key_when_null_expected.json create mode 100755 tests/acceptance/version_1/testcases/response/body/not_null_found_in_array_when_null_expected.json create mode 100755 tests/acceptance/version_1/testcases/response/body/null_found_at_key_where_not_null_expected.json create mode 100755 tests/acceptance/version_1/testcases/response/body/null_found_in_array_when_not_null_expected.json create mode 100755 tests/acceptance/version_1/testcases/response/body/number_found_at_key_when_string_expected.json create mode 100755 tests/acceptance/version_1/testcases/response/body/number_found_in_array_when_string_expected.json create mode 100755 tests/acceptance/version_1/testcases/response/body/objects_in_array_first_matches.json create mode 100755 tests/acceptance/version_1/testcases/response/body/objects_in_array_no_matches.json create mode 100755 tests/acceptance/version_1/testcases/response/body/objects_in_array_second_matches.json create mode 100755 tests/acceptance/version_1/testcases/response/body/plain_text_that_does_not_match.json create mode 100755 tests/acceptance/version_1/testcases/response/body/plain_text_that_matches.json create mode 100755 tests/acceptance/version_1/testcases/response/body/property_name_is_different_case.json create mode 100755 tests/acceptance/version_1/testcases/response/body/string_found_at_key_when_number_expected.json create mode 100755 tests/acceptance/version_1/testcases/response/body/string_found_in_array_when_number_expected.json create mode 100755 tests/acceptance/version_1/testcases/response/body/unexpected_index_with_not_null_value.json create mode 100755 tests/acceptance/version_1/testcases/response/body/unexpected_index_with_null_value.json create mode 100755 tests/acceptance/version_1/testcases/response/body/unexpected_key_with_not_null_value.json create mode 100755 tests/acceptance/version_1/testcases/response/body/unexpected_key_with_null_value.json create mode 100755 tests/acceptance/version_1/testcases/response/headers/empty_headers.json create mode 100755 tests/acceptance/version_1/testcases/response/headers/header_name_is_different_case.json create mode 100755 tests/acceptance/version_1/testcases/response/headers/header_value_is_different_case.json create mode 100755 tests/acceptance/version_1/testcases/response/headers/matches.json create mode 100755 tests/acceptance/version_1/testcases/response/headers/order_of_comma_separated_header_values_different.json create mode 100755 tests/acceptance/version_1/testcases/response/headers/unexpected_header_found.json create mode 100755 tests/acceptance/version_1/testcases/response/headers/whitespace_after_comma_different.json create mode 100644 tests/acceptance/version_1/testcases/response/status/different_status.json create mode 100644 tests/acceptance/version_1/testcases/response/status/different_status.py create mode 100755 tests/acceptance/version_1/testcases/response/status/matches.json diff --git a/pact_test/matchers/response_matcher.py b/pact_test/matchers/response_matcher.py index c0b656b..4dfe798 100644 --- a/pact_test/matchers/response_matcher.py +++ b/pact_test/matchers/response_matcher.py @@ -1,4 +1,5 @@ from pact_test.either import * +from pact_test.constants import FAILED def match(interaction, pact_response): @@ -42,8 +43,12 @@ def _to_dict(headers): def _build_error_message(section, expected, actual): - return 'Non-matching ' + section + ' for the response. Expected:\n\n\t' + \ - str(expected) + '\n\nReceived:\n\n\t' + str(actual) + return { + 'actual': actual, + 'status': FAILED, + 'expected': expected, + 'message': section.capitalize() + ' is incorrect' + } def _is_subset(expected, actual): diff --git a/tests/acceptance/__init__.py b/tests/acceptance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/acceptance/acceptance_test_loader.py b/tests/acceptance/acceptance_test_loader.py new file mode 100644 index 0000000..57b232d --- /dev/null +++ b/tests/acceptance/acceptance_test_loader.py @@ -0,0 +1,14 @@ +import os +import json + + +def load_acceptance_test(path_to_file): + acceptance_test_filename = os.path.basename(path_to_file).split('.py')[0] + acceptance_test_filename += '.json' + dir_name = os.path.dirname(path_to_file) + + path = os.path.join(dir_name, acceptance_test_filename) + with open(path) as f: + data = json.load(f) + + return data diff --git a/tests/acceptance/version_1/testcases/request/body/array in different order.json b/tests/acceptance/version_1/testcases/request/body/array in different order.json new file mode 100755 index 0000000..11d2c01 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/array in different order.json @@ -0,0 +1,26 @@ +{ + "match": false, + "comment": "Favourite colours in wrong order", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "favouriteColours": ["red","blue"] + } + } + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "favouriteColours": ["blue", "red"] + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/body/different value found at index.json b/tests/acceptance/version_1/testcases/request/body/different value found at index.json new file mode 100755 index 0000000..4fc0c10 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/different value found at index.json @@ -0,0 +1,26 @@ +{ + "match": false, + "comment": "Incorrect favourite colour", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "favouriteColours": ["red","blue"] + } + } + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "favouriteColours": ["red","taupe"] + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/body/different value found at key.json b/tests/acceptance/version_1/testcases/request/body/different value found at key.json new file mode 100755 index 0000000..12216ff --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/different value found at key.json @@ -0,0 +1,26 @@ +{ + "match": false, + "comment": "Incorrect value at alligator name", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "name": "Mary" + } + } + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "name": "Fred" + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/body/matches.json b/tests/acceptance/version_1/testcases/request/body/matches.json new file mode 100755 index 0000000..9f66c60 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/matches.json @@ -0,0 +1,30 @@ +{ + "match": true, + "comment": "Requests match", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "name": "Mary", + "feet": 4, + "favouriteColours": ["red","blue"] + } + } + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "feet": 4, + "name": "Mary", + "favouriteColours": ["red","blue"] + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/body/missing index.json b/tests/acceptance/version_1/testcases/request/body/missing index.json new file mode 100755 index 0000000..fe1c897 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/missing index.json @@ -0,0 +1,26 @@ +{ + "match": false, + "comment": "Missing favorite colour", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "favouriteColours": ["red","blue"] + } + } + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator": { + "favouriteColours": ["red"] + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/body/missing key.json b/tests/acceptance/version_1/testcases/request/body/missing key.json new file mode 100755 index 0000000..59c93a4 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/missing key.json @@ -0,0 +1,27 @@ +{ + "match": false, + "comment": "Missing key alligator name", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "name": "Mary", + "age": 3 + } + } + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator": { + "age": 3 + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/body/not null found at key when null expected.json b/tests/acceptance/version_1/testcases/request/body/not null found at key when null expected.json new file mode 100755 index 0000000..82c040f --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/not null found at key when null expected.json @@ -0,0 +1,26 @@ +{ + "match": false, + "comment": "Name should be null", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "name": null + } + } + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "name": "Fred" + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/body/not null found in array when null expected.json b/tests/acceptance/version_1/testcases/request/body/not null found in array when null expected.json new file mode 100755 index 0000000..871bf76 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/not null found in array when null expected.json @@ -0,0 +1,26 @@ +{ + "match": false, + "comment": "Favourite colours expected to contain null, but not null found", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "favouriteNumbers": ["1",null,"3"] + } + } + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "favouriteNumbers": ["1","2","3"] + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/body/null found at key where not null expected.json b/tests/acceptance/version_1/testcases/request/body/null found at key where not null expected.json new file mode 100755 index 0000000..94b80fb --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/null found at key where not null expected.json @@ -0,0 +1,26 @@ +{ + "match": false, + "comment": "Name should be null", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "name": "Mary" + } + } + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "name": null + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/body/null found in array when not null expected.json b/tests/acceptance/version_1/testcases/request/body/null found in array when not null expected.json new file mode 100755 index 0000000..42f9e4d --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/null found in array when not null expected.json @@ -0,0 +1,26 @@ +{ + "match": false, + "comment": "Favourite colours expected to be strings found a null", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "favouriteNumbers": ["1","2","3"] + } + } + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "favouriteNumbers": ["1",null,"3"] + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/body/number found at key when string expected.json b/tests/acceptance/version_1/testcases/request/body/number found at key when string expected.json new file mode 100755 index 0000000..957dfc1 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/number found at key when string expected.json @@ -0,0 +1,26 @@ +{ + "match": false, + "comment": "Number of feet expected to be string but was number", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "feet": "4" + } + } + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "feet": 4 + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/body/number found in array when string expected.json b/tests/acceptance/version_1/testcases/request/body/number found in array when string expected.json new file mode 100755 index 0000000..15664c1 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/number found in array when string expected.json @@ -0,0 +1,26 @@ +{ + "match": false, + "comment": "Favourite colours expected to be strings found a number", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "favouriteNumbers": ["1","2","3"] + } + } + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "favouriteNumbers": ["1",2,"3"] + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/body/plain text that does not match.json b/tests/acceptance/version_1/testcases/request/body/plain text that does not match.json new file mode 100755 index 0000000..08af3fa --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/plain text that does not match.json @@ -0,0 +1,18 @@ +{ + "match": false, + "comment": "Plain text that does not match", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": { "Content-Type": "text/plain" }, + "body": "alligator named mary" + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": { "Content-Type": "text/plain" }, + "body": "alligator named fred" + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/body/plain text that matches.json b/tests/acceptance/version_1/testcases/request/body/plain text that matches.json new file mode 100755 index 0000000..2afa70c --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/plain text that matches.json @@ -0,0 +1,18 @@ +{ + "match": true, + "comment": "Plain text that matches", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": { "Content-Type": "text/plain" }, + "body": "alligator named mary" + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": { "Content-Type": "text/plain" }, + "body": "alligator named mary" + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/body/string found at key when number expected.json b/tests/acceptance/version_1/testcases/request/body/string found at key when number expected.json new file mode 100755 index 0000000..9eaa5d1 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/string found at key when number expected.json @@ -0,0 +1,26 @@ +{ + "match": false, + "comment": "Number of feet expected to be number but was string", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "feet": 4 + } + } + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "feet": "4" + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/body/string found in array when number expected.json b/tests/acceptance/version_1/testcases/request/body/string found in array when number expected.json new file mode 100755 index 0000000..9948ad4 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/string found in array when number expected.json @@ -0,0 +1,26 @@ +{ + "match": false, + "comment": "Favourite Numbers expected to be numbers, but 2 is a string", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "favouriteNumbers": [1,2,3] + } + } + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "favouriteNumbers": [1,"2",3] + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/body/unexpected index with not null value.json b/tests/acceptance/version_1/testcases/request/body/unexpected index with not null value.json new file mode 100755 index 0000000..3e68632 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/unexpected index with not null value.json @@ -0,0 +1,26 @@ +{ + "match": false, + "comment": "Unexpected favourite colour", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "favouriteColours": ["red","blue"] + } + } + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "favouriteColours": ["red","blue","taupe"] + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/body/unexpected index with null value.json b/tests/acceptance/version_1/testcases/request/body/unexpected index with null value.json new file mode 100755 index 0000000..ca21b96 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/unexpected index with null value.json @@ -0,0 +1,26 @@ +{ + "match": false, + "comment": "Unexpected favourite colour with null value", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "favouriteColours": ["red","blue"] + } + } + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "favouriteColours": ["red","blue", null] + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/body/unexpected key with not null value.json b/tests/acceptance/version_1/testcases/request/body/unexpected key with not null value.json new file mode 100755 index 0000000..73a50f9 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/unexpected key with not null value.json @@ -0,0 +1,27 @@ +{ + "match": false, + "comment": "Unexpected phone number", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "name": "Mary" + } + } + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "name": "Mary", + "phoneNumber": "12345678" + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/body/unexpected key with null value.json b/tests/acceptance/version_1/testcases/request/body/unexpected key with null value.json new file mode 100755 index 0000000..68c6501 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/unexpected key with null value.json @@ -0,0 +1,27 @@ +{ + "match": false, + "comment": "Unexpected phone number with null value", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "name": "Mary" + } + } + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": {}, + "body": { + "alligator":{ + "name": "Mary", + "phoneNumber": null + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/headers/empty headers.json b/tests/acceptance/version_1/testcases/request/headers/empty headers.json new file mode 100755 index 0000000..cb6ca4a --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/headers/empty headers.json @@ -0,0 +1,17 @@ +{ + "match": true, + "comment": "Empty headers match", + "expected" : { + "method": "POST", + "path": "/path", + "query": "", + "headers": {} + + }, + "actual": { + "method": "POST", + "path": "/path", + "query": "", + "headers": {} + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/headers/header name is different case.json b/tests/acceptance/version_1/testcases/request/headers/header name is different case.json new file mode 100755 index 0000000..76f1de0 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/headers/header name is different case.json @@ -0,0 +1,20 @@ +{ + "match": true, + "comment": "Header name is case insensitive", + "expected" : { + "method": "POST", + "path": "/path", + "query": "", + "headers": { + "Accept": "alligators" + } + }, + "actual": { + "method": "POST", + "path": "/path", + "query": "", + "headers": { + "ACCEPT": "alligators" + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/headers/header value is different case.json b/tests/acceptance/version_1/testcases/request/headers/header value is different case.json new file mode 100755 index 0000000..9b10a7b --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/headers/header value is different case.json @@ -0,0 +1,20 @@ +{ + "match": false, + "comment": "Headers values are case sensitive", + "expected" : { + "method": "POST", + "path": "/path", + "query": "", + "headers": { + "Accept": "alligators" + } + }, + "actual": { + "method": "POST", + "path": "/path", + "query": "", + "headers": { + "Accept": "Alligators" + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/headers/matches.json b/tests/acceptance/version_1/testcases/request/headers/matches.json new file mode 100755 index 0000000..7a175c1 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/headers/matches.json @@ -0,0 +1,22 @@ +{ + "match": true, + "comment": "Headers match", + "expected" : { + "method": "POST", + "path": "/path", + "query": "", + "headers": { + "Accept": "alligators", + "Content-Type": "hippos" + } + }, + "actual": { + "method": "POST", + "path": "/path", + "query": "", + "headers": { + "Content-Type": "hippos", + "Accept": "alligators" + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/headers/order of comma separated header values different.json b/tests/acceptance/version_1/testcases/request/headers/order of comma separated header values different.json new file mode 100755 index 0000000..be6ffc4 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/headers/order of comma separated header values different.json @@ -0,0 +1,20 @@ +{ + "match": false, + "comment": "Comma separated headers out of order, order can matter http://tools.ietf.org/html/rfc2616", + "expected" : { + "method": "POST", + "path": "/path", + "query": "", + "headers": { + "Accept": "alligators, hippos" + } + }, + "actual": { + "method": "POST", + "path": "/path", + "query": "", + "headers": { + "Accept": "hippos, alligators" + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/headers/unexpected header found.json b/tests/acceptance/version_1/testcases/request/headers/unexpected header found.json new file mode 100755 index 0000000..d4b910d --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/headers/unexpected header found.json @@ -0,0 +1,18 @@ +{ + "match": true, + "comment": "Extra headers allowed", + "expected" : { + "method": "POST", + "path": "/path", + "query": "", + "headers": {} + }, + "actual": { + "method": "POST", + "path": "/path", + "query": "", + "headers": { + "Accept": "alligators" + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/headers/whitespace after comma different.json b/tests/acceptance/version_1/testcases/request/headers/whitespace after comma different.json new file mode 100755 index 0000000..64ce2f0 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/headers/whitespace after comma different.json @@ -0,0 +1,20 @@ +{ + "match": true, + "comment": "Whitespace between comma separated headers does not matter", + "expected" : { + "method": "POST", + "path": "/path", + "query": "", + "headers": { + "Accept": "alligators,hippos" + } + }, + "actual": { + "method": "POST", + "path": "/path", + "query": "", + "headers": { + "Accept": "alligators, hippos" + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/method/different method.json b/tests/acceptance/version_1/testcases/request/method/different method.json new file mode 100755 index 0000000..fe600d8 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/method/different method.json @@ -0,0 +1,17 @@ +{ + "match": false, + "comment": "Methods is incorrect", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {} + }, + "actual": { + "method": "GET", + "path": "/", + "query": "", + "headers": {} + + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/method/matches.json b/tests/acceptance/version_1/testcases/request/method/matches.json new file mode 100755 index 0000000..be94961 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/method/matches.json @@ -0,0 +1,17 @@ +{ + "match": true, + "comment": "Methods match", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {} + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": {} + + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/method/method is different case.json b/tests/acceptance/version_1/testcases/request/method/method is different case.json new file mode 100755 index 0000000..135c86d --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/method/method is different case.json @@ -0,0 +1,17 @@ +{ + "match": true, + "comment": "Methods case does not matter", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {} + }, + "actual": { + "method": "post", + "path": "/", + "query": "", + "headers": {} + + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/path/empty path found when forward slash expected.json b/tests/acceptance/version_1/testcases/request/path/empty path found when forward slash expected.json new file mode 100755 index 0000000..11d0abf --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/path/empty path found when forward slash expected.json @@ -0,0 +1,18 @@ +{ + "match": false, + "comment": "Empty path found when forward slash expected", + "expected" : { + "method": "POST", + "path": "/", + "query": "", + "headers": {} + + }, + "actual": { + "method": "POST", + "path": "", + "query": "", + "headers": {} + + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/path/forward slash found when empty path expected.json b/tests/acceptance/version_1/testcases/request/path/forward slash found when empty path expected.json new file mode 100755 index 0000000..ab7a7cb --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/path/forward slash found when empty path expected.json @@ -0,0 +1,18 @@ +{ + "match": false, + "comment": "Foward slash found when empty path expected", + "expected" : { + "method": "POST", + "path": "", + "query": "", + "headers": {} + + }, + "actual": { + "method": "POST", + "path": "/", + "query": "", + "headers": {} + + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/path/incorrect path.json b/tests/acceptance/version_1/testcases/request/path/incorrect path.json new file mode 100755 index 0000000..c968c72 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/path/incorrect path.json @@ -0,0 +1,18 @@ +{ + "match": false, + "comment": "Paths do not match", + "expected" : { + "method": "POST", + "path": "/path/to/something", + "query": "", + "headers": {} + + }, + "actual": { + "method": "POST", + "path": "/path/to/something/else", + "query": "", + "headers": {} + + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/path/matches.json b/tests/acceptance/version_1/testcases/request/path/matches.json new file mode 100755 index 0000000..e0880df --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/path/matches.json @@ -0,0 +1,18 @@ +{ + "match": true, + "comment": "Paths match", + "expected" : { + "method": "POST", + "path": "/path/to/something", + "query": "", + "headers": {} + + }, + "actual": { + "method": "POST", + "path": "/path/to/something", + "query": "", + "headers": {} + + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/path/missing trailing slash in path.json b/tests/acceptance/version_1/testcases/request/path/missing trailing slash in path.json new file mode 100755 index 0000000..cfd72d4 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/path/missing trailing slash in path.json @@ -0,0 +1,18 @@ +{ + "match": false, + "comment": "Path is missing trailing slash, trailing slashes can matter", + "expected" : { + "method": "POST", + "path": "/path/to/something/", + "query": "", + "headers": {} + + }, + "actual": { + "method": "POST", + "path": "/path/to/something", + "query": "", + "headers": {} + + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/path/unexpected trailing slash in path.json b/tests/acceptance/version_1/testcases/request/path/unexpected trailing slash in path.json new file mode 100755 index 0000000..e6b4434 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/path/unexpected trailing slash in path.json @@ -0,0 +1,18 @@ +{ + "match": false, + "comment": "Path has unexpected trailing slash, trailing slashes can matter", + "expected" : { + "method": "POST", + "path": "/path/to/something", + "query": "", + "headers": {} + + }, + "actual": { + "method": "POST", + "path": "/path/to/something/", + "query": "", + "headers": {} + + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/query/different param order.json b/tests/acceptance/version_1/testcases/request/query/different param order.json new file mode 100755 index 0000000..eb6f185 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/query/different param order.json @@ -0,0 +1,18 @@ +{ + "match": false, + "comment": "Query strings are matched using basic string equality, these are not equal.", + "expected" : { + "method": "GET", + "path": "/path", + "query": "alligator=Mary&hippo=John", + "headers": {} + + }, + "actual": { + "method": "GET", + "path": "/path", + "query": "hippo=John&alligator=Mary", + "headers": {} + + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/query/different param values.json b/tests/acceptance/version_1/testcases/request/query/different param values.json new file mode 100755 index 0000000..6dbc909 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/query/different param values.json @@ -0,0 +1,18 @@ +{ + "match": false, + "comment": "Queries are not the same - hippo is Fred instead of John", + "expected" : { + "method": "GET", + "path": "/path", + "query": "alligator=Mary&hippo=John", + "headers": {} + + }, + "actual": { + "method": "GET", + "path": "/path", + "query": "alligator=Mary&hippo=Fred", + "headers": {} + + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/query/matches with equals in the query value.json b/tests/acceptance/version_1/testcases/request/query/matches with equals in the query value.json new file mode 100755 index 0000000..21a47c4 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/query/matches with equals in the query value.json @@ -0,0 +1,18 @@ +{ + "match": true, + "comment": "Queries are equivalent", + "expected" : { + "method": "GET", + "path": "/path", + "query": "options=delete.topic.enable=true&broker=1", + "headers": {} + + }, + "actual": { + "method": "GET", + "path": "/path", + "query": "options=delete.topic.enable%3Dtrue&broker=1", + "headers": {} + + } +} diff --git a/tests/acceptance/version_1/testcases/request/query/matches.json b/tests/acceptance/version_1/testcases/request/query/matches.json new file mode 100755 index 0000000..79289ee --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/query/matches.json @@ -0,0 +1,18 @@ +{ + "match": true, + "comment": "Queries are the same", + "expected" : { + "method": "GET", + "path": "/path", + "query": "alligator=Mary&hippo=John", + "headers": {} + + }, + "actual": { + "method": "GET", + "path": "/path", + "query": "alligator=Mary&hippo=John", + "headers": {} + + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/request/query/trailing amperand.json b/tests/acceptance/version_1/testcases/request/query/trailing amperand.json new file mode 100755 index 0000000..eafffb0 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/query/trailing amperand.json @@ -0,0 +1,18 @@ +{ + "match": false, + "comment": "Query strings are matched using basic string equality, these are not equal.", + "expected" : { + "method": "GET", + "path": "/path", + "query": "alligator=Mary&hippo=John", + "headers": {} + + }, + "actual": { + "method": "GET", + "path": "/path", + "query": "alligator=Mary&hippo=John&", + "headers": {} + + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/array_in_different_order.json b/tests/acceptance/version_1/testcases/response/body/array_in_different_order.json new file mode 100755 index 0000000..ea5f3c9 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/array_in_different_order.json @@ -0,0 +1,20 @@ +{ + "match": false, + "comment": "Favourite colours in wrong order", + "expected" : { + "headers": {}, + "body": { + "alligator":{ + "favouriteColours": ["red","blue"] + } + } + }, + "actual": { + "headers": {}, + "body": { + "alligator":{ + "favouriteColours": ["blue", "red"] + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/deeply_nested_objects.json b/tests/acceptance/version_1/testcases/response/body/deeply_nested_objects.json new file mode 100755 index 0000000..47c2394 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/deeply_nested_objects.json @@ -0,0 +1,42 @@ +{ + "match": true, + "comment": "Comparisons should work even on nested objects", + "expected" : { + "headers": {}, + "body": { + "object1": { + "object2": { + "object4": { + "object5": { + "name": "Mary", + "friends": ["Fred", "John"] + }, + "object6": { + "phoneNumber": 1234567890 + } + } + } + } + } + }, + "actual": { + "headers": {}, + "body": { + "object1":{ + "object2": { + "object4":{ + "object5": { + "name": "Mary", + "friends": ["Fred", "John"], + "gender": "F" + }, + "object6": { + "phoneNumber": 1234567890 + } + } + }, + "color": "red" + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/different_value_found_at_index.json b/tests/acceptance/version_1/testcases/response/body/different_value_found_at_index.json new file mode 100755 index 0000000..774c5f5 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/different_value_found_at_index.json @@ -0,0 +1,20 @@ +{ + "match": false, + "comment": "Incorrect favourite colour", + "expected" : { + "headers": {}, + "body": { + "alligator":{ + "favouriteColours": ["red","blue"] + } + } + }, + "actual": { + "headers": {}, + "body": { + "alligator":{ + "favouriteColours": ["red","taupe"] + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/different_value_found_at_key.json b/tests/acceptance/version_1/testcases/response/body/different_value_found_at_key.json new file mode 100755 index 0000000..72a035d --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/different_value_found_at_key.json @@ -0,0 +1,20 @@ +{ + "match": false, + "comment": "Incorrect value at alligator name", + "expected" : { + "headers": {}, + "body": { + "alligator":{ + "name": "Mary" + } + } + }, + "actual": { + "headers": {}, + "body": { + "alligator":{ + "name": "Fred" + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/keys_out_of_order_match.json b/tests/acceptance/version_1/testcases/response/body/keys_out_of_order_match.json new file mode 100755 index 0000000..3901cbe --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/keys_out_of_order_match.json @@ -0,0 +1,18 @@ +{ + "match": true, + "comment": "Favourite number and favourite colours out of order", + "expected" : { + "headers": {}, + "body": { + "favouriteNumber": 7, + "favouriteColours": ["red","blue"] + } + }, + "actual": { + "headers": {}, + "body": { + "favouriteColours": ["red","blue"], + "favouriteNumber": 7 + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/matches.json b/tests/acceptance/version_1/testcases/response/body/matches.json new file mode 100755 index 0000000..c36e8f3 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/matches.json @@ -0,0 +1,24 @@ +{ + "match": true, + "comment": "Responses match", + "expected" : { + "headers": {}, + "body": { + "alligator":{ + "name": "Mary", + "feet": 4, + "favouriteColours": ["red","blue"] + } + } + }, + "actual": { + "headers": {}, + "body": { + "alligator":{ + "feet": 4, + "name": "Mary", + "favouriteColours": ["red","blue"] + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/missing_index.json b/tests/acceptance/version_1/testcases/response/body/missing_index.json new file mode 100755 index 0000000..3c08100 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/missing_index.json @@ -0,0 +1,20 @@ +{ + "match": false, + "comment": "Missing favorite colour", + "expected" : { + "headers": {}, + "body": { + "alligator":{ + "favouriteColours": ["red","blue"] + } + } + }, + "actual": { + "headers": {}, + "body": { + "alligator": { + "favouriteColours": ["red"] + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/missing_key.json b/tests/acceptance/version_1/testcases/response/body/missing_key.json new file mode 100755 index 0000000..fbfbb7a --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/missing_key.json @@ -0,0 +1,21 @@ +{ + "match": false, + "comment": "Missing key alligator name", + "expected" : { + "headers": {}, + "body": { + "alligator":{ + "name": "Mary", + "age": 3 + } + } + }, + "actual": { + "headers": {}, + "body": { + "alligator": { + "age": 3 + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/not_null_found_at_key_when_null_expected.json b/tests/acceptance/version_1/testcases/response/body/not_null_found_at_key_when_null_expected.json new file mode 100755 index 0000000..54aa2a4 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/not_null_found_at_key_when_null_expected.json @@ -0,0 +1,20 @@ +{ + "match": false, + "comment": "Name should be null", + "expected" : { + "headers": {}, + "body": { + "alligator":{ + "name": null + } + } + }, + "actual": { + "headers": {}, + "body": { + "alligator":{ + "name": "Fred" + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/not_null_found_in_array_when_null_expected.json b/tests/acceptance/version_1/testcases/response/body/not_null_found_in_array_when_null_expected.json new file mode 100755 index 0000000..3058dc7 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/not_null_found_in_array_when_null_expected.json @@ -0,0 +1,20 @@ +{ + "match": false, + "comment": "Favourite numbers expected to contain null, but not null found", + "expected" : { + "headers": {}, + "body": { + "alligator":{ + "favouriteNumbers": ["1",null,"3"] + } + } + }, + "actual": { + "headers": {}, + "body": { + "alligator":{ + "favouriteNumbers": ["1","2","3"] + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/null_found_at_key_where_not_null_expected.json b/tests/acceptance/version_1/testcases/response/body/null_found_at_key_where_not_null_expected.json new file mode 100755 index 0000000..46cef2a --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/null_found_at_key_where_not_null_expected.json @@ -0,0 +1,20 @@ +{ + "match": false, + "comment": "Name should not be null", + "expected" : { + "headers": {}, + "body": { + "alligator":{ + "name": "Mary" + } + } + }, + "actual": { + "headers": {}, + "body": { + "alligator":{ + "name": null + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/null_found_in_array_when_not_null_expected.json b/tests/acceptance/version_1/testcases/response/body/null_found_in_array_when_not_null_expected.json new file mode 100755 index 0000000..250647a --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/null_found_in_array_when_not_null_expected.json @@ -0,0 +1,20 @@ +{ + "match": false, + "comment": "Favourite numbers expected to be strings found a null", + "expected" : { + "headers": {}, + "body": { + "alligator":{ + "favouriteNumbers": ["1","2","3"] + } + } + }, + "actual": { + "headers": {}, + "body": { + "alligator":{ + "favouriteNumbers": ["1",null,"3"] + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/number_found_at_key_when_string_expected.json b/tests/acceptance/version_1/testcases/response/body/number_found_at_key_when_string_expected.json new file mode 100755 index 0000000..cb47ca8 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/number_found_at_key_when_string_expected.json @@ -0,0 +1,20 @@ +{ + "match": false, + "comment": "Number of feet expected to be string but was number", + "expected" : { + "headers": {}, + "body": { + "alligator":{ + "feet": "4" + } + } + }, + "actual": { + "headers": {}, + "body": { + "alligator":{ + "feet": 4 + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/number_found_in_array_when_string_expected.json b/tests/acceptance/version_1/testcases/response/body/number_found_in_array_when_string_expected.json new file mode 100755 index 0000000..daea5c7 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/number_found_in_array_when_string_expected.json @@ -0,0 +1,20 @@ +{ + "match": false, + "comment": "Favourite numbers expected to be strings found a number", + "expected" : { + "headers": {}, + "body": { + "alligator":{ + "favouriteNumbers": ["1","2","3"] + } + } + }, + "actual": { + "headers": {}, + "body": { + "alligator":{ + "favouriteNumbers": ["1",2,"3"] + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/objects_in_array_first_matches.json b/tests/acceptance/version_1/testcases/response/body/objects_in_array_first_matches.json new file mode 100755 index 0000000..334a74e --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/objects_in_array_first_matches.json @@ -0,0 +1,19 @@ +{ + "match": false, + "comment": "Properties match but unexpected element recieved", + "expected" : { + "headers": {}, + "body": [ + {"favouriteColor": "red"} + ] + }, + "actual": { + "headers": {}, + "body": [ + {"favouriteColor": "red", + "favouriteNumber": 2}, + {"favouriteColor": "blue", + "favouriteNumber": 2} + ] + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/objects_in_array_no_matches.json b/tests/acceptance/version_1/testcases/response/body/objects_in_array_no_matches.json new file mode 100755 index 0000000..c5eb3b7 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/objects_in_array_no_matches.json @@ -0,0 +1,20 @@ +{ + "match": false, + "comment": "Array of objects, properties match on incorrect objects", + "expected" : { + "headers": {}, + "body": [ + {"favouriteColor": "red"}, + {"favouriteNumber": 2} + ] + }, + "actual": { + "headers": {}, + "body": [ + {"favouriteColor": "blue", + "favouriteNumber": 4}, + {"favouriteColor": "red", + "favouriteNumber": 2} + ] + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/objects_in_array_second_matches.json b/tests/acceptance/version_1/testcases/response/body/objects_in_array_second_matches.json new file mode 100755 index 0000000..dc87a61 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/objects_in_array_second_matches.json @@ -0,0 +1,19 @@ +{ + "match": false, + "comment": "Property of second object matches, but unexpected element recieved", + "expected" : { + "headers": {}, + "body": [ + {"favouriteColor": "red"} + ] + }, + "actual": { + "headers": {}, + "body": [ + {"favouriteColor": "blue", + "favouriteNumber": 4}, + {"favouriteColor": "red", + "favouriteNumber": 2} + ] + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/plain_text_that_does_not_match.json b/tests/acceptance/version_1/testcases/response/body/plain_text_that_does_not_match.json new file mode 100755 index 0000000..937e2f0 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/plain_text_that_does_not_match.json @@ -0,0 +1,12 @@ +{ + "match": false, + "comment": "Plain text that does not match", + "expected" : { + "headers": { "Content-Type": "text/plain" }, + "body": "alligator named mary" + }, + "actual": { + "headers": { "Content-Type": "text/plain" }, + "body": "alligator named fred" + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/plain_text_that_matches.json b/tests/acceptance/version_1/testcases/response/body/plain_text_that_matches.json new file mode 100755 index 0000000..961f36b --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/plain_text_that_matches.json @@ -0,0 +1,12 @@ +{ + "match": true, + "comment": "Plain text that matches", + "expected" : { + "headers": { "Content-Type": "text/plain" }, + "body": "alligator named mary" + }, + "actual": { + "headers": { "Content-Type": "text/plain" }, + "body": "alligator named mary" + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/property_name_is_different_case.json b/tests/acceptance/version_1/testcases/response/body/property_name_is_different_case.json new file mode 100755 index 0000000..b4ff784 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/property_name_is_different_case.json @@ -0,0 +1,20 @@ +{ + "match": false, + "comment": "Property names on objects are case sensitive", + "expected" : { + "headers": {}, + "body": { + "alligator":{ + "FavouriteColour": "red" + } + } + }, + "actual": { + "headers": {}, + "body": { + "alligator":{ + "favouritecolour": "red" + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/string_found_at_key_when_number_expected.json b/tests/acceptance/version_1/testcases/response/body/string_found_at_key_when_number_expected.json new file mode 100755 index 0000000..194d397 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/string_found_at_key_when_number_expected.json @@ -0,0 +1,20 @@ +{ + "match": false, + "comment": "Number of feet expected to be number but was string", + "expected" : { + "headers": {}, + "body": { + "alligator":{ + "feet": 4 + } + } + }, + "actual": { + "headers": {}, + "body": { + "alligator":{ + "feet": "4" + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/string_found_in_array_when_number_expected.json b/tests/acceptance/version_1/testcases/response/body/string_found_in_array_when_number_expected.json new file mode 100755 index 0000000..bec0a6b --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/string_found_in_array_when_number_expected.json @@ -0,0 +1,20 @@ +{ + "match": false, + "comment": "Favourite Numbers expected to be numbers, but 2 is a string", + "expected" : { + "headers": {}, + "body": { + "alligator":{ + "favouriteNumbers": [1,2,3] + } + } + }, + "actual": { + "headers": {}, + "body": { + "alligator":{ + "favouriteNumbers": [1,"2",3] + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/unexpected_index_with_not_null_value.json b/tests/acceptance/version_1/testcases/response/body/unexpected_index_with_not_null_value.json new file mode 100755 index 0000000..9064245 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/unexpected_index_with_not_null_value.json @@ -0,0 +1,20 @@ +{ + "match": false, + "comment": "Unexpected favourite colour", + "expected" : { + "headers": {}, + "body": { + "alligator":{ + "favouriteColours": ["red","blue"] + } + } + }, + "actual": { + "headers": {}, + "body": { + "alligator":{ + "favouriteColours": ["red","blue","taupe"] + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/unexpected_index_with_null_value.json b/tests/acceptance/version_1/testcases/response/body/unexpected_index_with_null_value.json new file mode 100755 index 0000000..fbb3b91 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/unexpected_index_with_null_value.json @@ -0,0 +1,20 @@ +{ + "match": false, + "comment": "Unexpected favourite colour with null value", + "expected" : { + "headers": {}, + "body": { + "alligator":{ + "favouriteColours": ["red","blue"] + } + } + }, + "actual": { + "headers": {}, + "body": { + "alligator":{ + "favouriteColours": ["red","blue", null] + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/unexpected_key_with_not_null_value.json b/tests/acceptance/version_1/testcases/response/body/unexpected_key_with_not_null_value.json new file mode 100755 index 0000000..487baa9 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/unexpected_key_with_not_null_value.json @@ -0,0 +1,21 @@ +{ + "match": true, + "comment": "Unexpected phone number", + "expected" : { + "headers": {}, + "body": { + "alligator":{ + "name": "Mary" + } + } + }, + "actual": { + "headers": {}, + "body": { + "alligator":{ + "name": "Mary", + "phoneNumber": "12345678" + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/body/unexpected_key_with_null_value.json b/tests/acceptance/version_1/testcases/response/body/unexpected_key_with_null_value.json new file mode 100755 index 0000000..202954c --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/unexpected_key_with_null_value.json @@ -0,0 +1,21 @@ +{ + "match": true, + "comment": "Unexpected phone number with null value", + "expected" : { + "headers": {}, + "body": { + "alligator":{ + "name": "Mary" + } + } + }, + "actual": { + "headers": {}, + "body": { + "alligator":{ + "name": "Mary", + "phoneNumber": null + } + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/headers/empty_headers.json b/tests/acceptance/version_1/testcases/response/headers/empty_headers.json new file mode 100755 index 0000000..7b0db77 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/headers/empty_headers.json @@ -0,0 +1,11 @@ +{ + "match": true, + "comment": "Empty headers match", + "expected" : { + "headers": {} + + }, + "actual": { + "headers": {} + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/headers/header_name_is_different_case.json b/tests/acceptance/version_1/testcases/response/headers/header_name_is_different_case.json new file mode 100755 index 0000000..2c2e50d --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/headers/header_name_is_different_case.json @@ -0,0 +1,14 @@ +{ + "match": true, + "comment": "Header name is case insensitive", + "expected" : { + "headers": { + "Accept": "alligators" + } + }, + "actual": { + "headers": { + "ACCEPT": "alligators" + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/headers/header_value_is_different_case.json b/tests/acceptance/version_1/testcases/response/headers/header_value_is_different_case.json new file mode 100755 index 0000000..1bc8d03 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/headers/header_value_is_different_case.json @@ -0,0 +1,14 @@ +{ + "match": false, + "comment": "Headers values are case sensitive", + "expected" : { + "headers": { + "Accept": "alligators" + } + }, + "actual": { + "headers": { + "Accept": "Alligators" + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/headers/matches.json b/tests/acceptance/version_1/testcases/response/headers/matches.json new file mode 100755 index 0000000..36b4b8b --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/headers/matches.json @@ -0,0 +1,16 @@ +{ + "match": true, + "comment": "Headers match", + "expected" : { + "headers": { + "Accept": "alligators", + "Content-Type": "hippos" + } + }, + "actual": { + "headers": { + "Content-Type": "hippos", + "Accept": "alligators" + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/headers/order_of_comma_separated_header_values_different.json b/tests/acceptance/version_1/testcases/response/headers/order_of_comma_separated_header_values_different.json new file mode 100755 index 0000000..fd4edc8 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/headers/order_of_comma_separated_header_values_different.json @@ -0,0 +1,14 @@ +{ + "match": false, + "comment": "Comma separated headers out of order, order can matter http://tools.ietf.org/html/rfc2616", + "expected" : { + "headers": { + "Accept": "alligators, hippos" + } + }, + "actual": { + "headers": { + "Accept": "hippos, alligators" + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/headers/unexpected_header_found.json b/tests/acceptance/version_1/testcases/response/headers/unexpected_header_found.json new file mode 100755 index 0000000..74849ef --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/headers/unexpected_header_found.json @@ -0,0 +1,12 @@ +{ + "match": true, + "comment": "Extra headers allowed", + "expected" : { + "headers": {} + }, + "actual": { + "headers": { + "Accept": "alligators" + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/headers/whitespace_after_comma_different.json b/tests/acceptance/version_1/testcases/response/headers/whitespace_after_comma_different.json new file mode 100755 index 0000000..6f85a84 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/headers/whitespace_after_comma_different.json @@ -0,0 +1,14 @@ +{ + "match": true, + "comment": "Whitespace between comma separated headers does not matter", + "expected" : { + "headers": { + "Accept": "alligators,hippos" + } + }, + "actual": { + "headers": { + "Accept": "alligators, hippos" + } + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/status/different_status.json b/tests/acceptance/version_1/testcases/response/status/different_status.json new file mode 100644 index 0000000..0f978c4 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/status/different_status.json @@ -0,0 +1,10 @@ +{ + "match": false, + "comment": "Status is incorrect", + "expected" : { + "status" : 202 + }, + "actual" : { + "status" : 400 + } +} \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/status/different_status.py b/tests/acceptance/version_1/testcases/response/status/different_status.py new file mode 100644 index 0000000..2163eae --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/status/different_status.py @@ -0,0 +1,19 @@ +from pact_test.constants import * +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_status(): + data = load_acceptance_test(__file__) + + response = PactResponse(status=400) + interaction = {'response': {'status': data['expected']['status']}} + test_result = match(interaction, response).value + + assert test_result == { + 'status': FAILED, + 'message': 'Status is incorrect', + 'expected': data['expected']['status'], + 'actual': 400 + } diff --git a/tests/acceptance/version_1/testcases/response/status/matches.json b/tests/acceptance/version_1/testcases/response/status/matches.json new file mode 100755 index 0000000..a96ceaa --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/status/matches.json @@ -0,0 +1,10 @@ +{ + "match": true, + "comment": "Status matches", + "expected" : { + "status" : 202 + }, + "actual" : { + "status" : 202 + } +} \ No newline at end of file diff --git a/tests/matchers/response_matcher.py b/tests/matchers/response_matcher.py index 123f9f6..fa48912 100644 --- a/tests/matchers/response_matcher.py +++ b/tests/matchers/response_matcher.py @@ -22,17 +22,24 @@ def valid_response(): def test_non_matching_status(): pact_response = PactResponse(status=200) - msg = 'Non-matching status for the response. Expected:\n\n\t' + \ - str(418) + '\n\nReceived:\n\n\t' + str(200) + msg = { + 'status': 'FAILED', + 'message': 'Status is incorrect', + 'expected': 418, + 'actual': 200 + } assert match(interaction, pact_response).value == msg def test_non_matching_headers(): pact_response = PactResponse(status=418, headers=[('Date', '12-06-2017')]) - msg = 'Non-matching headers for the response. Expected:\n\n\t' + \ - str({'Content-Type': 'spam'}) + '\n\nReceived:\n\n\t' + \ - str({'Date': '12-06-2017'}) + msg = { + 'status': 'FAILED', + 'message': 'Headers is incorrect', + 'expected': {'Content-Type': 'spam'}, + 'actual': {'Date': '12-06-2017'} + } assert match(interaction, pact_response).value == msg @@ -43,9 +50,12 @@ def test_non_matching_body(): headers=[('Content-Type', 'spam')], body={'spam': 'spam'} ) - msg = 'Non-matching body for the response. Expected:\n\n\t' + \ - str({'spam': 'eggs'}) + '\n\nReceived:\n\n\t' + \ - str({'spam': 'spam'}) + msg = { + 'status': 'FAILED', + 'message': 'Body is incorrect', + 'expected': {'spam': 'eggs'}, + 'actual': {'spam': 'spam'} + } assert match(interaction, pact_response).value == msg diff --git a/tests/runners/service_consumers/test_suite.py b/tests/runners/service_consumers/test_suite.py index 685a620..6513f05 100644 --- a/tests/runners/service_consumers/test_suite.py +++ b/tests/runners/service_consumers/test_suite.py @@ -35,7 +35,7 @@ def connect(_, **kwargs): assert test_outcome['state'] == 'the breakfast is available' assert test_outcome['status'] == 'FAILED' assert len(test_outcome['errors']) == 1 - assert test_outcome['errors'][0].startswith('Non-matching headers') + assert test_outcome['errors'][0]['message'] == 'Headers is incorrect' def test_verify_bad_pact(): From 413dacab9bf1824ced2ca00de004868c4d278245 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sun, 25 Jun 2017 16:17:12 +1000 Subject: [PATCH 37/85] Status matches test. --- .../response/status/different_status.py | 2 +- .../testcases/response/status/matches.json | 18 +++++++++--------- .../testcases/response/status/matches.py | 14 ++++++++++++++ 3 files changed, 24 insertions(+), 10 deletions(-) mode change 100755 => 100644 tests/acceptance/version_1/testcases/response/status/matches.json create mode 100644 tests/acceptance/version_1/testcases/response/status/matches.py diff --git a/tests/acceptance/version_1/testcases/response/status/different_status.py b/tests/acceptance/version_1/testcases/response/status/different_status.py index 2163eae..ec2b60a 100644 --- a/tests/acceptance/version_1/testcases/response/status/different_status.py +++ b/tests/acceptance/version_1/testcases/response/status/different_status.py @@ -13,7 +13,7 @@ def test_different_status(): assert test_result == { 'status': FAILED, - 'message': 'Status is incorrect', + 'message': data['comment'], 'expected': data['expected']['status'], 'actual': 400 } diff --git a/tests/acceptance/version_1/testcases/response/status/matches.json b/tests/acceptance/version_1/testcases/response/status/matches.json old mode 100755 new mode 100644 index a96ceaa..a754762 --- a/tests/acceptance/version_1/testcases/response/status/matches.json +++ b/tests/acceptance/version_1/testcases/response/status/matches.json @@ -1,10 +1,10 @@ -{ - "match": true, - "comment": "Status matches", - "expected" : { - "status" : 202 - }, - "actual" : { - "status" : 202 - } +{ + "match": true, + "comment": "Status matches", + "expected": { + "status": 202 + }, + "actual": { + "status": 202 + } } \ No newline at end of file diff --git a/tests/acceptance/version_1/testcases/response/status/matches.py b/tests/acceptance/version_1/testcases/response/status/matches.py new file mode 100644 index 0000000..050402f --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/status/matches.py @@ -0,0 +1,14 @@ +from pact_test.either import Right +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_status(): + data = load_acceptance_test(__file__) + + response = PactResponse(status=202) + interaction = {'response': {'status': data['expected']['status']}} + test_result = match(interaction, response) + + assert type(test_result) is Right From 79d3a30b4b9108115bdb996918459513c7aed5f0 Mon Sep 17 00:00:00 2001 From: Guido Barbaglia Date: Thu, 29 Jun 2017 17:44:35 +1000 Subject: [PATCH 38/85] Test suite runner to always return Either. --- pact_test/runners/service_consumers/test_suite.py | 2 +- tests/runners/service_consumers/test_suite.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pact_test/runners/service_consumers/test_suite.py b/pact_test/runners/service_consumers/test_suite.py index ca5cd09..1a6219d 100644 --- a/pact_test/runners/service_consumers/test_suite.py +++ b/pact_test/runners/service_consumers/test_suite.py @@ -19,7 +19,7 @@ def verify(self): if type(pact_helper) is Right: self.pact_helper = pact_helper.value tests = self.collect_tests().value - return list(map(self.verify_test, tests)) + return Right(list(map(self.verify_test, tests))) return pact_helper def verify_test(self, test): diff --git a/tests/runners/service_consumers/test_suite.py b/tests/runners/service_consumers/test_suite.py index 6513f05..d3acf3b 100644 --- a/tests/runners/service_consumers/test_suite.py +++ b/tests/runners/service_consumers/test_suite.py @@ -25,7 +25,7 @@ def connect(_, **kwargs): return Response() monkeypatch.setattr(requests, 'request', connect) - actual_response = sct.verify() + actual_response = sct.verify().value assert len(actual_response) == 1 assert actual_response[0].value['test'] == 'TestRestaurantCustomer' @@ -44,7 +44,7 @@ def test_verify_bad_pact(): 'resources', 'service_consumers_bad_pact') sct = ServiceConsumerTestSuiteRunner(config) - actual_response = sct.verify() + actual_response = sct.verify().value assert type(actual_response[0]) is Left From 720cbc8653effa2268d8e4a1f7c612067e2d0209 Mon Sep 17 00:00:00 2001 From: Guido Barbaglia Date: Thu, 29 Jun 2017 19:00:37 +1000 Subject: [PATCH 39/85] WIP headers matching. --- pact_test/matchers/response_matcher.py | 9 ++++++--- .../response/headers/empty_headers.json | 1 - .../testcases/response/headers/empty_headers.py | 14 ++++++++++++++ .../headers/header_name_is_different_case.py | 14 ++++++++++++++ .../{matches.json => headers_match.json} | 0 .../testcases/response/headers/headers_match.py | 17 +++++++++++++++++ .../{matches.json => status_matches.json} | 0 .../status/{matches.py => status_matches.py} | 0 tests/models/response.py | 2 +- 9 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 tests/acceptance/version_1/testcases/response/headers/empty_headers.py create mode 100644 tests/acceptance/version_1/testcases/response/headers/header_name_is_different_case.py rename tests/acceptance/version_1/testcases/response/headers/{matches.json => headers_match.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/response/headers/headers_match.py rename tests/acceptance/version_1/testcases/response/status/{matches.json => status_matches.json} (100%) rename tests/acceptance/version_1/testcases/response/status/{matches.py => status_matches.py} (100%) diff --git a/pact_test/matchers/response_matcher.py b/pact_test/matchers/response_matcher.py index 4dfe798..f16e6f0 100644 --- a/pact_test/matchers/response_matcher.py +++ b/pact_test/matchers/response_matcher.py @@ -12,16 +12,19 @@ def _match_status(interaction, pact_response): expected = interaction['response'].get('status') actual = pact_response.status - if actual == expected: + if expected is None or actual == expected: return Right(interaction) return Left(_build_error_message('status', expected, actual)) def _match_headers(interaction, pact_response): - expected = interaction['response'].get('headers') + expected = interaction['response'].get('headers', {}) actual = _to_dict(pact_response.headers) - if _is_subset(expected, actual): + insensitive_expected = {k.upper(): v for (k, v) in expected.items()} + insensitive_actual = {k.upper(): v for (k, v) in actual.items()} + + if _is_subset(insensitive_expected, insensitive_actual): return Right(interaction) return Left(_build_error_message('headers', expected, actual)) diff --git a/tests/acceptance/version_1/testcases/response/headers/empty_headers.json b/tests/acceptance/version_1/testcases/response/headers/empty_headers.json index 7b0db77..c10c032 100755 --- a/tests/acceptance/version_1/testcases/response/headers/empty_headers.json +++ b/tests/acceptance/version_1/testcases/response/headers/empty_headers.json @@ -3,7 +3,6 @@ "comment": "Empty headers match", "expected" : { "headers": {} - }, "actual": { "headers": {} diff --git a/tests/acceptance/version_1/testcases/response/headers/empty_headers.py b/tests/acceptance/version_1/testcases/response/headers/empty_headers.py new file mode 100644 index 0000000..5e0d643 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/headers/empty_headers.py @@ -0,0 +1,14 @@ +from pact_test.either import Right +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_status(): + data = load_acceptance_test(__file__) + + response = PactResponse(headers=[]) + interaction = {'response': {'headers': data['expected']['headers']}} + test_result = match(interaction, response) + + assert type(test_result) is Right diff --git a/tests/acceptance/version_1/testcases/response/headers/header_name_is_different_case.py b/tests/acceptance/version_1/testcases/response/headers/header_name_is_different_case.py new file mode 100644 index 0000000..65291ac --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/headers/header_name_is_different_case.py @@ -0,0 +1,14 @@ +from pact_test.either import Right +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_status(): + data = load_acceptance_test(__file__) + + response = PactResponse(headers=[('ACCEPT', 'alligators')]) + interaction = {'response': {'headers': data['expected']['headers']}} + test_result = match(interaction, response) + + assert type(test_result) is Right diff --git a/tests/acceptance/version_1/testcases/response/headers/matches.json b/tests/acceptance/version_1/testcases/response/headers/headers_match.json similarity index 100% rename from tests/acceptance/version_1/testcases/response/headers/matches.json rename to tests/acceptance/version_1/testcases/response/headers/headers_match.json diff --git a/tests/acceptance/version_1/testcases/response/headers/headers_match.py b/tests/acceptance/version_1/testcases/response/headers/headers_match.py new file mode 100644 index 0000000..3f81ed7 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/headers/headers_match.py @@ -0,0 +1,17 @@ +from pact_test.either import Right +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_status(): + data = load_acceptance_test(__file__) + + response = PactResponse(headers=[ + ('Accept', 'alligators'), + ('Content-Type', 'hippos') + ]) + interaction = {'response': {'headers': data['expected']['headers']}} + test_result = match(interaction, response) + + assert type(test_result) is Right diff --git a/tests/acceptance/version_1/testcases/response/status/matches.json b/tests/acceptance/version_1/testcases/response/status/status_matches.json similarity index 100% rename from tests/acceptance/version_1/testcases/response/status/matches.json rename to tests/acceptance/version_1/testcases/response/status/status_matches.json diff --git a/tests/acceptance/version_1/testcases/response/status/matches.py b/tests/acceptance/version_1/testcases/response/status/status_matches.py similarity index 100% rename from tests/acceptance/version_1/testcases/response/status/matches.py rename to tests/acceptance/version_1/testcases/response/status/status_matches.py diff --git a/tests/models/response.py b/tests/models/response.py index 3b65a82..c46cc8a 100644 --- a/tests/models/response.py +++ b/tests/models/response.py @@ -6,7 +6,7 @@ def test_default_body(): assert r.body is None -def test_deafult_headers(): +def test_default_headers(): r = PactResponse() assert r.headers == [] From 37c6173fe9a45490b3e57c9e9884cbc71bba7155 Mon Sep 17 00:00:00 2001 From: Guido Barbaglia Date: Tue, 22 Aug 2017 16:44:14 +1000 Subject: [PATCH 40/85] Acceptance tests: Response Headers --- pact_test/matchers/response_matcher.py | 12 +++++++++++- .../testcases/response/headers/empty_headers.py | 2 +- .../headers/header_name_is_different_case.py | 2 +- .../headers/header_value_is_different_case.py | 14 ++++++++++++++ .../testcases/response/headers/headers_match.py | 2 +- ...r_of_comma_separated_header_values_different.py | 14 ++++++++++++++ .../response/headers/unexpected_header_found.py | 14 ++++++++++++++ .../headers/whitespace_after_comma_different.py | 14 ++++++++++++++ 8 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 tests/acceptance/version_1/testcases/response/headers/header_value_is_different_case.py create mode 100644 tests/acceptance/version_1/testcases/response/headers/order_of_comma_separated_header_values_different.py create mode 100644 tests/acceptance/version_1/testcases/response/headers/unexpected_header_found.py create mode 100644 tests/acceptance/version_1/testcases/response/headers/whitespace_after_comma_different.py diff --git a/pact_test/matchers/response_matcher.py b/pact_test/matchers/response_matcher.py index f16e6f0..1b27eb6 100644 --- a/pact_test/matchers/response_matcher.py +++ b/pact_test/matchers/response_matcher.py @@ -58,4 +58,14 @@ def _is_subset(expected, actual): actual_items = actual.items() if actual else {} expected_items = expected.items() if expected else {} - return all(item in expected_items for item in actual_items) + stripped_actual_items = map(_strip_whitespaces_after_commas, actual_items) + stripped_expected_items = map(_strip_whitespaces_after_commas, expected_items) + + return all(item in stripped_actual_items for item in stripped_expected_items) + + +def _strip_whitespaces_after_commas(t): + k = t[0] + v = t[1].replace(', ', ',') if type(t[1]) is str else t[1] + + return k, v diff --git a/tests/acceptance/version_1/testcases/response/headers/empty_headers.py b/tests/acceptance/version_1/testcases/response/headers/empty_headers.py index 5e0d643..5ef0fbd 100644 --- a/tests/acceptance/version_1/testcases/response/headers/empty_headers.py +++ b/tests/acceptance/version_1/testcases/response/headers/empty_headers.py @@ -4,7 +4,7 @@ from tests.acceptance.acceptance_test_loader import load_acceptance_test -def test_different_status(): +def test_empty_headers(): data = load_acceptance_test(__file__) response = PactResponse(headers=[]) diff --git a/tests/acceptance/version_1/testcases/response/headers/header_name_is_different_case.py b/tests/acceptance/version_1/testcases/response/headers/header_name_is_different_case.py index 65291ac..f9651ff 100644 --- a/tests/acceptance/version_1/testcases/response/headers/header_name_is_different_case.py +++ b/tests/acceptance/version_1/testcases/response/headers/header_name_is_different_case.py @@ -4,7 +4,7 @@ from tests.acceptance.acceptance_test_loader import load_acceptance_test -def test_different_status(): +def test_different_name_case(): data = load_acceptance_test(__file__) response = PactResponse(headers=[('ACCEPT', 'alligators')]) diff --git a/tests/acceptance/version_1/testcases/response/headers/header_value_is_different_case.py b/tests/acceptance/version_1/testcases/response/headers/header_value_is_different_case.py new file mode 100644 index 0000000..b54be8f --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/headers/header_value_is_different_case.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_value_case(): + data = load_acceptance_test(__file__) + + response = PactResponse(headers=[('Accept', 'Alligators')]) + interaction = {'response': {'headers': data['expected']['headers']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/headers/headers_match.py b/tests/acceptance/version_1/testcases/response/headers/headers_match.py index 3f81ed7..304db14 100644 --- a/tests/acceptance/version_1/testcases/response/headers/headers_match.py +++ b/tests/acceptance/version_1/testcases/response/headers/headers_match.py @@ -4,7 +4,7 @@ from tests.acceptance.acceptance_test_loader import load_acceptance_test -def test_different_status(): +def test_headers_match(): data = load_acceptance_test(__file__) response = PactResponse(headers=[ diff --git a/tests/acceptance/version_1/testcases/response/headers/order_of_comma_separated_header_values_different.py b/tests/acceptance/version_1/testcases/response/headers/order_of_comma_separated_header_values_different.py new file mode 100644 index 0000000..d0e4539 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/headers/order_of_comma_separated_header_values_different.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_order_of_headers(): + data = load_acceptance_test(__file__) + + response = PactResponse(headers=[('Accept', 'hippos, alligators')]) + interaction = {'response': {'headers': data['expected']['headers']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/headers/unexpected_header_found.py b/tests/acceptance/version_1/testcases/response/headers/unexpected_header_found.py new file mode 100644 index 0000000..ce3e81d --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/headers/unexpected_header_found.py @@ -0,0 +1,14 @@ +from pact_test.either import Right +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_unexpected_header(): + data = load_acceptance_test(__file__) + + response = PactResponse(headers=[('Accept', 'alligators')]) + interaction = {'response': {'headers': data['expected']['headers']}} + test_result = match(interaction, response) + + assert type(test_result) is Right diff --git a/tests/acceptance/version_1/testcases/response/headers/whitespace_after_comma_different.py b/tests/acceptance/version_1/testcases/response/headers/whitespace_after_comma_different.py new file mode 100644 index 0000000..789ed96 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/headers/whitespace_after_comma_different.py @@ -0,0 +1,14 @@ +from pact_test.either import Right +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_whitespaces_after_comma(): + data = load_acceptance_test(__file__) + + response = PactResponse(headers=[('Accept', 'alligators, hippos')]) + interaction = {'response': {'headers': data['expected']['headers']}} + test_result = match(interaction, response) + + assert type(test_result) is Right From ff821be70e48645987618400af82d421f057a6a1 Mon Sep 17 00:00:00 2001 From: Guido Barbaglia Date: Wed, 23 Aug 2017 11:19:10 +1000 Subject: [PATCH 41/85] Setup and Tear Down invoked for the suite rather than single test. --- pact_test/constants/__init__.py | 1 + .../runners/service_consumers/state_test.py | 2 -- .../runners/service_consumers/test_suite.py | 18 +++++++++++++++--- pact_test/utils/logger.py | 9 +++++++++ tests/runners/service_consumers/state_test.py | 2 -- 5 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 pact_test/utils/logger.py diff --git a/pact_test/constants/__init__.py b/pact_test/constants/__init__.py index 5e7c6ce..8d4322d 100644 --- a/pact_test/constants/__init__.py +++ b/pact_test/constants/__init__.py @@ -4,6 +4,7 @@ PASSED = 'PASSED' FAILED = 'FAILED' PROVIDER_STATE = 'providerState' +TEST_PARENT = 'ServiceConsumerTest' MISSING_STATE = 'Missing implementation for state ' MISSING_PACT_HELPER = 'Missing "pact_helper.py" at "' MISSING_PACT_URI = 'Missing setup for "pact_uri" at ' diff --git a/pact_test/runners/service_consumers/state_test.py b/pact_test/runners/service_consumers/state_test.py index 50631f3..4193957 100644 --- a/pact_test/runners/service_consumers/state_test.py +++ b/pact_test/runners/service_consumers/state_test.py @@ -7,13 +7,11 @@ def verify_state(interaction, pact_helper, test_instance): state = find_state(interaction, test_instance) if type(state) is Right: - pact_helper.setup() state.value() output = _execute_request(pact_helper, interaction) if type(output) is Right: response_verification = match(interaction, output.value) output = _build_state_response(state, response_verification) - pact_helper.tear_down() return output return state diff --git a/pact_test/runners/service_consumers/test_suite.py b/pact_test/runners/service_consumers/test_suite.py index 1a6219d..7f3d5f0 100644 --- a/pact_test/runners/service_consumers/test_suite.py +++ b/pact_test/runners/service_consumers/test_suite.py @@ -6,6 +6,8 @@ from pact_test.utils.pact_utils import get_pact from pact_test.utils.pact_helper_utils import load_pact_helper from pact_test.runners.service_consumers.state_test import verify_state +from pact_test.utils.logger import info +from pact_test.utils.logger import error class ServiceConsumerTestSuiteRunner(object): @@ -15,11 +17,21 @@ def __init__(self, config): self.config = config def verify(self): + info('Verify consumers: START') pact_helper = load_pact_helper(self.config.consumer_tests_path) if type(pact_helper) is Right: self.pact_helper = pact_helper.value - tests = self.collect_tests().value - return Right(list(map(self.verify_test, tests))) + tests = self.collect_tests() + if type(tests) is Right: + self.pact_helper.setup() + test_results = Right(list(map(self.verify_test, tests.value))) + self.pact_helper.tear_down() + return test_results + error('Verify consumers: EXIT WITH ERRORS:') + error(tests.value) + return tests + error('Verify consumers: EXIT WITH ERRORS:') + error(pact_helper.value) return pact_helper def verify_test(self, test): @@ -43,7 +55,7 @@ def collect_tests(self): for name, obj in inspect.getmembers(test): if inspect.isclass(obj) and len(inspect.getmro(obj)) > 2: test_parent = inspect.getmro(obj)[1].__name__ - if test_parent == 'ServiceConsumerTest': + if test_parent == TEST_PARENT: tests.append(obj()) if not files: diff --git a/pact_test/utils/logger.py b/pact_test/utils/logger.py new file mode 100644 index 0000000..165b855 --- /dev/null +++ b/pact_test/utils/logger.py @@ -0,0 +1,9 @@ +PREFIX = '[Pact Test for Python] - ' # pragma: no cover + + +def info(message): # pragma: no cover + print('\033[92m' + PREFIX + message + '\033[0m') # pragma: no cover + + +def error(message): # pragma: no cover + print('\033[91m' + PREFIX + message + '\033[0m') # pragma: no cover diff --git a/tests/runners/service_consumers/state_test.py b/tests/runners/service_consumers/state_test.py index e388c8f..0e6a2d0 100644 --- a/tests/runners/service_consumers/state_test.py +++ b/tests/runners/service_consumers/state_test.py @@ -87,8 +87,6 @@ def json(self): 'errors': [] } - assert pact_helper.setup.call_count == 1 - assert pact_helper.tear_down.call_count == 1 assert response == expected_response From b550157baf251bfaf9f11d0552e87d3118296692 Mon Sep 17 00:00:00 2001 From: Guido Barbaglia Date: Wed, 23 Aug 2017 16:18:09 +1000 Subject: [PATCH 42/85] Error message for non extending PactHelper --- README.rst | 9 ++++++++- pact_test/constants/__init__.py | 1 + pact_test/utils/pact_helper_utils.py | 3 +++ setup.py | 2 +- .../pact_helper_no_super_class/pact_helper.py | 3 +++ tests/utils/pact_helper_utils.py | 13 ++++++++++++- 6 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 tests/resources/pact_helper_no_super_class/pact_helper.py diff --git a/README.rst b/README.rst index d73753a..7e6635a 100644 --- a/README.rst +++ b/README.rst @@ -45,4 +45,11 @@ possible to test all the versions at once with: .. code:: bash - $ ./bin/test all \ No newline at end of file + $ ./bin/test all + +Upload New Version +------------------ + +.. code:: bash + + $ python3 setup.py sdist upload diff --git a/pact_test/constants/__init__.py b/pact_test/constants/__init__.py index 8d4322d..c02fca4 100644 --- a/pact_test/constants/__init__.py +++ b/pact_test/constants/__init__.py @@ -12,3 +12,4 @@ MISSING_SETUP = 'Missing "setup" method in "pact_helper.py".' MISSING_HAS_PACT_WITH = 'Missing setup for "has_pact_with" at ' MISSING_TEAR_DOWN = 'Missing "tear_down" method in "pact_helper.py".' +EXTEND_PACT_HELPER = 'Pact Helper class must extend pact_test.PactHelper' diff --git a/pact_test/utils/pact_helper_utils.py b/pact_test/utils/pact_helper_utils.py index 9d4aea4..ad23354 100644 --- a/pact_test/utils/pact_helper_utils.py +++ b/pact_test/utils/pact_helper_utils.py @@ -5,6 +5,7 @@ from pact_test import PactHelper from pact_test.constants import MISSING_SETUP from pact_test.constants import MISSING_TEAR_DOWN +from pact_test.constants import EXTEND_PACT_HELPER from pact_test.constants import MISSING_PACT_HELPER @@ -21,6 +22,8 @@ def _load_user_class(user_module): if inspect.isclass(obj) and len(inspect.getmro(obj)) > 2 and issubclass(obj, PactHelper): user_class = obj() + if user_class is None: + return Left(EXTEND_PACT_HELPER) if hasattr(user_class, 'setup') is False: return Left(MISSING_SETUP) if hasattr(user_class, 'tear_down') is False: diff --git a/setup.py b/setup.py index 0cd550a..ced4b3d 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.1.2', + version='0.1.7', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), diff --git a/tests/resources/pact_helper_no_super_class/pact_helper.py b/tests/resources/pact_helper_no_super_class/pact_helper.py new file mode 100644 index 0000000..fd8a6c6 --- /dev/null +++ b/tests/resources/pact_helper_no_super_class/pact_helper.py @@ -0,0 +1,3 @@ +class MyPactHelper(object): + def setup(self): + pass diff --git a/tests/utils/pact_helper_utils.py b/tests/utils/pact_helper_utils.py index e0e32b1..3ed2cd5 100644 --- a/tests/utils/pact_helper_utils.py +++ b/tests/utils/pact_helper_utils.py @@ -39,7 +39,8 @@ def test_load_user_class_missing_setup(): def test_load_user_class_missing_tear_down(): consumer_tests_path = os.path.join(os.getcwd(), 'tests', - 'resources', 'pact_helper_no_tear_down') + 'resources', + 'pact_helper_no_tear_down') pact_helper = load_pact_helper(consumer_tests_path).value error_message = 'Missing "tear_down" method in "pact_helper.py".' @@ -51,3 +52,13 @@ def test_load_pact_helper(): 'resources', 'pact_helper') pact_helper = load_pact_helper(consumer_tests_path).value assert issubclass(pact_helper.__class__, PactHelper) + + +def test_not_extending_pact_helper(): + consumer_tests_path = os.path.join(os.getcwd(), 'tests', + 'resources', + 'pact_helper_no_super_class') + pact_helper = load_pact_helper(consumer_tests_path).value + + error_message = 'Pact Helper class must extend pact_test.PactHelper' + assert pact_helper == error_message From c091782126407ea3af1dbfd6df5a51cfb41b3834 Mon Sep 17 00:00:00 2001 From: Guido Barbaglia Date: Thu, 24 Aug 2017 11:00:57 +1000 Subject: [PATCH 43/85] Add user logs --- pact_test/runners/pact_tests_runner.py | 23 ++++++++++++++- .../runners/service_consumers/state_test.py | 28 ++++++++++++------- .../runners/service_consumers/test_suite.py | 15 ++++++++-- pact_test/utils/logger.py | 9 ++++-- pact_test/utils/pact_utils.py | 3 ++ setup.py | 2 +- tests/runners/service_consumers/state_test.py | 8 ++++-- 7 files changed, 69 insertions(+), 19 deletions(-) diff --git a/pact_test/runners/pact_tests_runner.py b/pact_test/runners/pact_tests_runner.py index b457837..1c8bb93 100644 --- a/pact_test/runners/pact_tests_runner.py +++ b/pact_test/runners/pact_tests_runner.py @@ -1,3 +1,5 @@ +from pact_test.either import * +from pact_test.utils.logger import * from pact_test.config.config_builder import Config from pact_test.runners.service_consumers.test_suite import ServiceConsumerTestSuiteRunner from pact_test.runners.service_providers.provider_tests_runner import ProviderTestsRunner @@ -13,7 +15,26 @@ def verify(verify_consumers=False, verify_providers=False): def run_consumer_tests(config): - ServiceConsumerTestSuiteRunner(config).verify() + test_results = ServiceConsumerTestSuiteRunner(config).verify() + if type(test_results) is Left: + error(test_results.value) + else: + if type(test_results.value) is Left: + error(test_results.value.value) + else: + for test_result in test_results.value: + print() + info('Test: ' + test_result.value['test']) + for result in test_result.value['results']: + info(' GIVEN ' + result.value['state'] + ' UPON RECEIVING ' + result.value['description']) + info(' status: ' + result.value['status']) + for test_error in result.value['errors']: + error(' expected: ' + str(test_error['expected'])) + error(' actual: ' + str(test_error['actual'])) + error(' message: ' + str(test_error['message'])) + info('') + info('Goodbye!') + print() def run_provider_tests(config): diff --git a/pact_test/runners/service_consumers/state_test.py b/pact_test/runners/service_consumers/state_test.py index 4193957..c618a4a 100644 --- a/pact_test/runners/service_consumers/state_test.py +++ b/pact_test/runners/service_consumers/state_test.py @@ -1,18 +1,25 @@ -from pact_test.matchers.response_matcher import match from pact_test.either import * from pact_test.constants import * +from pact_test.utils.logger import * +from pact_test.matchers.response_matcher import match from pact_test.clients.http_client import execute_interaction_request def verify_state(interaction, pact_helper, test_instance): - state = find_state(interaction, test_instance) + state = find_state(interaction, interaction['description'], test_instance) if type(state) is Right: + debug('Verify state: "' + str(state.value.state) + '"') + debug('Setup state for test: START') state.value() + debug('Setup state for test: DONE') output = _execute_request(pact_helper, interaction) if type(output) is Right: response_verification = match(interaction, output.value) - output = _build_state_response(state, response_verification) + output = _build_state_response(state, interaction['description'], response_verification) + return output + error(output.value) return output + error(state.value) return state @@ -22,22 +29,23 @@ def _execute_request(pact_helper, interaction): return execute_interaction_request(url, port, interaction) -def _build_state_response(state, response_verification): +def _build_state_response(state, description, response_verification): if type(response_verification) is Right: - return Right(_format_message(state.value.state, PASSED, [])) + return Right(_format_message(state.value.state, description, PASSED, [])) else: errors = [response_verification.value] - return Left(_format_message(state.value.state, FAILED, errors)) + debug(response_verification.value) + return Left(_format_message(state.value.state, description, FAILED, errors)) -def find_state(interaction, test_instance): +def find_state(interaction, description, test_instance): state = interaction[PROVIDER_STATE] for s in test_instance.states: if s.state == state: return Right(s) message = 'Missing state implementation for "' + state + '"' - return Left(_format_message(state, FAILED, [message])) + return Left(_format_message(state, description, FAILED, [message])) -def _format_message(state, status, errors): - return {'state': state, 'status': status, 'errors': errors} +def _format_message(state, description, status, errors): + return {'state': state, 'description': description, 'status': status, 'errors': errors} diff --git a/pact_test/runners/service_consumers/test_suite.py b/pact_test/runners/service_consumers/test_suite.py index 7f3d5f0..aaa28e8 100644 --- a/pact_test/runners/service_consumers/test_suite.py +++ b/pact_test/runners/service_consumers/test_suite.py @@ -3,11 +3,10 @@ import inspect from pact_test.either import * from pact_test.constants import * +from pact_test.utils.logger import * from pact_test.utils.pact_utils import get_pact from pact_test.utils.pact_helper_utils import load_pact_helper from pact_test.runners.service_consumers.state_test import verify_state -from pact_test.utils.logger import info -from pact_test.utils.logger import error class ServiceConsumerTestSuiteRunner(object): @@ -17,15 +16,22 @@ def __init__(self, config): self.config = config def verify(self): - info('Verify consumers: START') + print() + debug('Verify consumers: START') pact_helper = load_pact_helper(self.config.consumer_tests_path) if type(pact_helper) is Right: self.pact_helper = pact_helper.value tests = self.collect_tests() if type(tests) is Right: + debug(str(len(tests.value)) + ' test(s) found.') + debug('Execute Pact Helper setup: START') self.pact_helper.setup() + debug('Execute Pact Helper setup: DONE') test_results = Right(list(map(self.verify_test, tests.value))) + debug('Execute Pact Helper tear down: START') self.pact_helper.tear_down() + debug('Execute Pact Helper tear down: DONE') + debug('Verify consumers: DONE') return test_results error('Verify consumers: EXIT WITH ERRORS:') error(tests.value) @@ -40,9 +46,12 @@ def verify_test(self, test): pact = get_pact(test.pact_uri) if type(pact) is Right: interactions = pact.value.get('interactions', {}) + debug(str(len(interactions)) + ' interaction(s) found') test_results = [verify_state(i, self.pact_helper, test) for i in interactions] return Right({'test': test.__class__.__name__, 'results': test_results}) + error(pact.value) return pact + error(validity_check.value) return validity_check def collect_tests(self): diff --git a/pact_test/utils/logger.py b/pact_test/utils/logger.py index 165b855..f18ad71 100644 --- a/pact_test/utils/logger.py +++ b/pact_test/utils/logger.py @@ -2,8 +2,13 @@ def info(message): # pragma: no cover - print('\033[92m' + PREFIX + message + '\033[0m') # pragma: no cover + print('\033[92m' + PREFIX + str(message) + '\033[0m') # pragma: no cover def error(message): # pragma: no cover - print('\033[91m' + PREFIX + message + '\033[0m') # pragma: no cover + print('\033[91m' + PREFIX + str(message) + '\033[0m') # pragma: no cover + + +def debug(message): # pragma: no cover + # print('\033[93m' + PREFIX + str(message) + '\033[0m') # pragma: no cover + pass diff --git a/pact_test/utils/pact_utils.py b/pact_test/utils/pact_utils.py index 12df1ea..71150d7 100644 --- a/pact_test/utils/pact_utils.py +++ b/pact_test/utils/pact_utils.py @@ -1,6 +1,7 @@ import json import requests from pact_test.either import * +from pact_test.utils.logger import debug def get_pact(location): @@ -10,6 +11,7 @@ def get_pact(location): def __get_pact_from_file(filename): + debug('Get pact from file "' + str(filename) + '"') try: with open(filename) as file_content: return Right(json.loads(file_content.read())) @@ -18,6 +20,7 @@ def __get_pact_from_file(filename): def __get_pact_from_url(url): + debug('Get pact from URL "' + str(url) + '"') try: return Right(requests.get(url).json()) except Exception as e: diff --git a/setup.py b/setup.py index ced4b3d..68334af 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.1.7', + version='0.1.48', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), diff --git a/tests/runners/service_consumers/state_test.py b/tests/runners/service_consumers/state_test.py index 0e6a2d0..e232838 100644 --- a/tests/runners/service_consumers/state_test.py +++ b/tests/runners/service_consumers/state_test.py @@ -24,7 +24,7 @@ def test_get_book(self): def test_find_state(): - response = find_state(interaction, test_instance).value + response = find_state(interaction, '', test_instance).value assert type(response).__name__.endswith('method') @@ -38,6 +38,7 @@ def test_missing_state(): response = verify_state(i, pact_helper, test_instance).value expected_response = { 'state': 'Catch me if you can', + 'description': 'Description', 'status': 'FAILED', 'errors': ['Missing state implementation for "Catch me if you can"'] } @@ -52,12 +53,13 @@ class BadTest(ServiceConsumerTest): bad_test_instance = BadTest() expected_response = { 'state': 'some books exist', + 'description': 'Description', 'status': 'FAILED', 'errors': [ 'Missing state implementation for "some books exist"' ] } - response = find_state(interaction, bad_test_instance).value + response = find_state(interaction, 'Description', bad_test_instance).value assert response == expected_response @@ -83,6 +85,7 @@ def json(self): response = verify_state(interaction, pact_helper, test_instance).value expected_response = { 'state': 'some books exist', + 'description': 'Description', 'status': 'PASSED', 'errors': [] } @@ -92,6 +95,7 @@ def json(self): interaction = { 'providerState': 'some books exist', + 'description': 'Description', 'request': { 'method': 'GET', 'path': '', From 407367c3db7a822d623159b499a3f9eabc005487 Mon Sep 17 00:00:00 2001 From: Guido Barbaglia Date: Thu, 24 Aug 2017 16:02:29 +1000 Subject: [PATCH 44/85] Acceptance tests for body --- README.rst | 125 +++++++++++++++++- pact_test/matchers/response_matcher.py | 10 +- .../response/body/array_in_different_order.py | 18 +++ .../body/different_value_found_at_key.py | 14 ++ .../testcases/response/body/matches.py | 23 ++++ .../testcases/response/body/missing_index.py | 14 ++ .../testcases/response/body/missing_key.py | 14 ++ ...ot_null_found_at_key_when_null_expected.py | 14 ++ ...ll_found_at_key_where_not_null_expected.py | 14 ++ ...umber_found_at_key_when_string_expected.py | 14 ++ .../body/plain_text_that_does_not_match.py | 14 ++ .../response/body/plain_text_that_matches.py | 14 ++ .../body/property_name_is_different_case.py | 14 ++ ...tring_found_at_key_when_number_expected.py | 14 ++ 14 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 tests/acceptance/version_1/testcases/response/body/array_in_different_order.py create mode 100644 tests/acceptance/version_1/testcases/response/body/different_value_found_at_key.py create mode 100644 tests/acceptance/version_1/testcases/response/body/matches.py create mode 100644 tests/acceptance/version_1/testcases/response/body/missing_index.py create mode 100644 tests/acceptance/version_1/testcases/response/body/missing_key.py create mode 100644 tests/acceptance/version_1/testcases/response/body/not_null_found_at_key_when_null_expected.py create mode 100644 tests/acceptance/version_1/testcases/response/body/null_found_at_key_where_not_null_expected.py create mode 100644 tests/acceptance/version_1/testcases/response/body/number_found_at_key_when_string_expected.py create mode 100644 tests/acceptance/version_1/testcases/response/body/plain_text_that_does_not_match.py create mode 100644 tests/acceptance/version_1/testcases/response/body/plain_text_that_matches.py create mode 100644 tests/acceptance/version_1/testcases/response/body/property_name_is_different_case.py create mode 100644 tests/acceptance/version_1/testcases/response/body/string_found_at_key_when_number_expected.py diff --git a/README.rst b/README.rst index 7e6635a..bfb212e 100644 --- a/README.rst +++ b/README.rst @@ -14,9 +14,130 @@ :target: https://saythanks.io/to/Kalimaha Pact Test for Python -=============== +==================== -Python implementation for Pact (http://pact.io/) +This repository contains a Python implementation for `Pact `_. Pact is a specification for +Consumer Driven Contracts Testing. For further information about Pact project, contracts testing, pros and cons and +useful resources please refer to the `Pact website `_. + +There are two phases in Consumer Driven Contracts Testing: a Consumer sets up a contract (*it's consumer driven +after all!*), and a Provider honours it. + +Providers Tests (*Set the Contracts*) +------------------------------------- + +.. image:: https://img.shields.io/badge/Pact-1.0-red.svg + :target: https://github.com/pact-foundation/pact-specification/tree/version-1 +.. image:: https://img.shields.io/badge/Pact-1.1-red.svg + :target: https://github.com/pact-foundation/pact-specification/tree/version-1.1 +.. image:: https://img.shields.io/badge/Pact-2.0-red.svg + :target: https://github.com/pact-foundation/pact-specification/tree/version-2 +.. image:: https://img.shields.io/badge/Pact-3.0-red.svg + :target: https://github.com/pact-foundation/pact-specification/tree/version-3 +.. image:: https://img.shields.io/badge/Pact-4.0-red.svg + :target: https://github.com/pact-foundation/pact-specification/tree/version-4 + +TBD. + +Consumers Tests (*Honour Your Contracts*) +----------------------------------------- + +.. image:: https://img.shields.io/badge/Pact-1.0-brightgreen.svg + :target: https://github.com/pact-foundation/pact-specification/tree/version-1 +.. image:: https://img.shields.io/badge/Pact-1.1-red.svg + :target: https://github.com/pact-foundation/pact-specification/tree/version-1.1 +.. image:: https://img.shields.io/badge/Pact-2.0-red.svg + :target: https://github.com/pact-foundation/pact-specification/tree/version-2 +.. image:: https://img.shields.io/badge/Pact-3.0-red.svg + :target: https://github.com/pact-foundation/pact-specification/tree/version-3 +.. image:: https://img.shields.io/badge/Pact-4.0-red.svg + :target: https://github.com/pact-foundation/pact-specification/tree/version-4 + +Providers run Consumer Tests to verify that they are honouring their pacts with the consumers. There are few examples +of an hypothetical restaurant service implemented with the most popular Python web frameworks: + +* Djanjo (*TODO*) +* `Flask `_ +* Pyramid (*TODO*) + +There are few things required to setup and run consumer tests. + +Pact Helper +~~~~~~~~~~~ + +This helper class is used by Pact Test to start and stop the web-app before and after the test. It also defines the +ports and endpoint to be used by the test. The following is an example of Pact Helper: + +.. code:: python + + class RestaurantPactHelper(PactHelper): + process = None + + def setup(self): + self.process = subprocess.Popen('gunicorn start:app -w 3 -b :8080 --log-level error', shell=True) + + def tear_down(self): + self.process.kill() + +There are few rules for the helper: + +* it **must** extend :code:`PactHelper` class from :code:`pact_test` +* it **must** define a :code:`setup` method +* it **must** define a :code:`tear_down` method + +It is also possible to override default url (*localhost*) and port (*9999*): + +.. code:: python + + class RestaurantPactHelper(PactHelper): + test_url = '0.0.0.0' + test_port = 5000 + + +States +~~~~~~ + +When a consumer sets a pact, it defines certain states. States are basically pre-requisites to your test. Before +honouring the pacts, a provider needs to define such states. For example: + +.. code:: python + + @has_pact_with('Restaurant Service') + @pact_uri('http://Kalimaha.github.io/src/pacts/pact.json') + class UberEats(ServiceConsumerTest): + + @state('some menu items exist') + def test_get_menu_items(self): + DB.save(MenuItem('spam')) + DB.save(MenuItem('eggs')) + +In this example, the provider stores some test data in its DB in order to make the system ready to receive mock calls +from the consumer and therefore verify the pact. + +Configuration +------------- + +The default configuration of Pact Test assumes the following values: + +* **consumer_tests_path:** :code:`tests/service_consumers` +* **provider_tests_path:** :code:`tests/service_providers` +* **pact_broker_uri:** :code:`None` + +It is possible to override such values by creating a file named :code:`.pact.json` in the project's root. The following +is an example of a valid configuration file: + +.. code:: json + + { + "consumer_tests_path": "mypath/mytests", + "provider_tests_path": "mypath/mytests", + "pact_broker_uri": "http://example.com/" + } + +All fields are optional: only specified fields will override default configuration values. + +Development +=========== Setup ----- diff --git a/pact_test/matchers/response_matcher.py b/pact_test/matchers/response_matcher.py index 1b27eb6..42a93c1 100644 --- a/pact_test/matchers/response_matcher.py +++ b/pact_test/matchers/response_matcher.py @@ -33,8 +33,16 @@ def _match_body(interaction, pact_response): expected = interaction['response'].get('body') actual = pact_response.body - if _is_subset(expected, actual): + if expected is None and actual is None: return Right(interaction) + + if type(expected) is str and type(actual) is str and expected == actual: + return Right(interaction) + + if type(expected) is dict and type(actual) is dict: + if _is_subset(expected, actual): + return Right(interaction) + return Left(_build_error_message('body', expected, actual)) diff --git a/tests/acceptance/version_1/testcases/response/body/array_in_different_order.py b/tests/acceptance/version_1/testcases/response/body/array_in_different_order.py new file mode 100644 index 0000000..a4c4774 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/array_in_different_order.py @@ -0,0 +1,18 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_array_in_different_order(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={ + 'alligator': { + 'favouriteColours': ['blue', 'red'] + } + }) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/body/different_value_found_at_key.py b/tests/acceptance/version_1/testcases/response/body/different_value_found_at_key.py new file mode 100644 index 0000000..b60ede6 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/different_value_found_at_key.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_value_at_key(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={'alligator': {'name': 'Fred'}}) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/body/matches.py b/tests/acceptance/version_1/testcases/response/body/matches.py new file mode 100644 index 0000000..f835bec --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/matches.py @@ -0,0 +1,23 @@ +from pact_test.either import Right +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_matches(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={ + 'alligator': { + 'feet': 4, + 'name': 'Mary', + 'favouriteColours': [ + 'red', + 'blue' + ] + } + }) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Right diff --git a/tests/acceptance/version_1/testcases/response/body/missing_index.py b/tests/acceptance/version_1/testcases/response/body/missing_index.py new file mode 100644 index 0000000..ee909af --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/missing_index.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_missing_index(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={'alligator': {'favouriteColours': ['red']}}) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/body/missing_key.py b/tests/acceptance/version_1/testcases/response/body/missing_key.py new file mode 100644 index 0000000..bec3c2e --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/missing_key.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_missing_key(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={'alligator': {'age': 3}}) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/body/not_null_found_at_key_when_null_expected.py b/tests/acceptance/version_1/testcases/response/body/not_null_found_at_key_when_null_expected.py new file mode 100644 index 0000000..ece9b18 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/not_null_found_at_key_when_null_expected.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_not_null(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={'alligator': {'name': 'Fred'}}) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/body/null_found_at_key_where_not_null_expected.py b/tests/acceptance/version_1/testcases/response/body/null_found_at_key_where_not_null_expected.py new file mode 100644 index 0000000..77df2fd --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/null_found_at_key_where_not_null_expected.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_not_null(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={'alligator': {'name': None}}) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/body/number_found_at_key_when_string_expected.py b/tests/acceptance/version_1/testcases/response/body/number_found_at_key_when_string_expected.py new file mode 100644 index 0000000..d74c8d0 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/number_found_at_key_when_string_expected.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_unexpected_number(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={'alligator': {'feet': 4}}) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/body/plain_text_that_does_not_match.py b/tests/acceptance/version_1/testcases/response/body/plain_text_that_does_not_match.py new file mode 100644 index 0000000..8e151c9 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/plain_text_that_does_not_match.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_non_matching_plain_text(): + data = load_acceptance_test(__file__) + + response = PactResponse(body='alligator named fred') + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/body/plain_text_that_matches.py b/tests/acceptance/version_1/testcases/response/body/plain_text_that_matches.py new file mode 100644 index 0000000..17c906c --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/plain_text_that_matches.py @@ -0,0 +1,14 @@ +from pact_test.either import Right +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_matching_plain_text(): + data = load_acceptance_test(__file__) + + response = PactResponse(body='alligator named mary') + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Right diff --git a/tests/acceptance/version_1/testcases/response/body/property_name_is_different_case.py b/tests/acceptance/version_1/testcases/response/body/property_name_is_different_case.py new file mode 100644 index 0000000..822fead --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/property_name_is_different_case.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_case(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={'alligator': {'favouritecolour': 'red'}}) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/body/string_found_at_key_when_number_expected.py b/tests/acceptance/version_1/testcases/response/body/string_found_at_key_when_number_expected.py new file mode 100644 index 0000000..df66986 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/string_found_at_key_when_number_expected.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_case(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={'alligator': {'feet': '4'}}) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left From c96ee37923af818606afda2456a2d8e111d2295b Mon Sep 17 00:00:00 2001 From: Guido Barbaglia Date: Fri, 25 Aug 2017 15:43:42 +1000 Subject: [PATCH 45/85] More tests for response body --- pact_test/matchers/response_matcher.py | 14 +++++++- .../response/body/deeply_nested_objects.py | 32 +++++++++++++++++++ .../body/different_value_found_at_index.py | 18 +++++++++++ .../response/body/keys_out_of_order_match.py | 19 +++++++++++ ..._null_found_in_array_when_null_expected.py | 18 +++++++++++ ...l_found_in_array_when_not_null_expected.py | 18 +++++++++++ ...ber_found_in_array_when_string_expected.py | 18 +++++++++++ .../body/objects_in_array_first_matches.py | 23 +++++++++++++ .../body/objects_in_array_no_matches.py | 23 +++++++++++++ .../body/objects_in_array_second_matches.py | 23 +++++++++++++ .../response/body/plain_text_that_matches.py | 2 ++ ...ing_found_in_array_when_number_expected.py | 18 +++++++++++ .../unexpected_index_with_not_null_value.py | 18 +++++++++++ .../body/unexpected_index_with_null_value.py | 18 +++++++++++ .../unexpected_key_with_not_null_value.py | 19 +++++++++++ .../body/unexpected_key_with_null_value.py | 19 +++++++++++ 16 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 tests/acceptance/version_1/testcases/response/body/deeply_nested_objects.py create mode 100644 tests/acceptance/version_1/testcases/response/body/different_value_found_at_index.py create mode 100644 tests/acceptance/version_1/testcases/response/body/keys_out_of_order_match.py create mode 100644 tests/acceptance/version_1/testcases/response/body/not_null_found_in_array_when_null_expected.py create mode 100644 tests/acceptance/version_1/testcases/response/body/null_found_in_array_when_not_null_expected.py create mode 100644 tests/acceptance/version_1/testcases/response/body/number_found_in_array_when_string_expected.py create mode 100644 tests/acceptance/version_1/testcases/response/body/objects_in_array_first_matches.py create mode 100644 tests/acceptance/version_1/testcases/response/body/objects_in_array_no_matches.py create mode 100644 tests/acceptance/version_1/testcases/response/body/objects_in_array_second_matches.py create mode 100644 tests/acceptance/version_1/testcases/response/body/string_found_in_array_when_number_expected.py create mode 100644 tests/acceptance/version_1/testcases/response/body/unexpected_index_with_not_null_value.py create mode 100644 tests/acceptance/version_1/testcases/response/body/unexpected_index_with_null_value.py create mode 100644 tests/acceptance/version_1/testcases/response/body/unexpected_key_with_not_null_value.py create mode 100644 tests/acceptance/version_1/testcases/response/body/unexpected_key_with_null_value.py diff --git a/pact_test/matchers/response_matcher.py b/pact_test/matchers/response_matcher.py index 42a93c1..ac5b5a0 100644 --- a/pact_test/matchers/response_matcher.py +++ b/pact_test/matchers/response_matcher.py @@ -40,12 +40,24 @@ def _match_body(interaction, pact_response): return Right(interaction) if type(expected) is dict and type(actual) is dict: - if _is_subset(expected, actual): + if _match_dicts(expected, actual): return Right(interaction) return Left(_build_error_message('body', expected, actual)) +def _match_dicts(expected, actual): + expected_keys = expected.keys() + actual_keys = actual.keys() + all_keys = set(actual_keys).issubset(set(expected_keys)) + + all_values = True + for (k1, v1), (k2, v2) in zip(actual.items(), expected.items()): + all_values = all_values and (v1 == v2) + + return all_keys and all_values + + def _to_dict(headers): d = {} for h in headers: diff --git a/tests/acceptance/version_1/testcases/response/body/deeply_nested_objects.py b/tests/acceptance/version_1/testcases/response/body/deeply_nested_objects.py new file mode 100644 index 0000000..bc393d2 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/deeply_nested_objects.py @@ -0,0 +1,32 @@ +import pytest +from pact_test.either import Right +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +@pytest.mark.skip(reason="requires further investigation") +def test_nested_objects(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={ + 'object1': { + 'object2': { + 'object4': { + 'object5': { + 'name': 'Mary', + 'friends': ['Fred', 'John'], + 'gender': 'F' + }, + 'object6': { + 'phoneNumber': 1234567890 + } + } + }, + 'color': 'red' + } + }) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Right diff --git a/tests/acceptance/version_1/testcases/response/body/different_value_found_at_index.py b/tests/acceptance/version_1/testcases/response/body/different_value_found_at_index.py new file mode 100644 index 0000000..1b1c44c --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/different_value_found_at_index.py @@ -0,0 +1,18 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_values(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={ + 'alligator': { + 'favouriteColours': ['red', 'taupe'] + } + }) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/body/keys_out_of_order_match.py b/tests/acceptance/version_1/testcases/response/body/keys_out_of_order_match.py new file mode 100644 index 0000000..94d6c77 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/keys_out_of_order_match.py @@ -0,0 +1,19 @@ +import pytest +from pact_test.either import Right +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +@pytest.mark.skip(reason="requires further investigation") +def test_different_values(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={ + 'favouriteColours': ['red', 'blue'], + 'favouriteNumber': 7 + }) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Right diff --git a/tests/acceptance/version_1/testcases/response/body/not_null_found_in_array_when_null_expected.py b/tests/acceptance/version_1/testcases/response/body/not_null_found_in_array_when_null_expected.py new file mode 100644 index 0000000..5a06d8f --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/not_null_found_in_array_when_null_expected.py @@ -0,0 +1,18 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_values(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={ + 'alligator': { + 'name': None + } + }) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/body/null_found_in_array_when_not_null_expected.py b/tests/acceptance/version_1/testcases/response/body/null_found_in_array_when_not_null_expected.py new file mode 100644 index 0000000..901b56a --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/null_found_in_array_when_not_null_expected.py @@ -0,0 +1,18 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_values(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={ + 'alligator': { + 'favouriteNumbers': ['1', None, '3'] + } + }) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/body/number_found_in_array_when_string_expected.py b/tests/acceptance/version_1/testcases/response/body/number_found_in_array_when_string_expected.py new file mode 100644 index 0000000..1d490ea --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/number_found_in_array_when_string_expected.py @@ -0,0 +1,18 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_values(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={ + 'alligator': { + 'favouriteNumbers': ['1', 2, '3'] + } + }) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/body/objects_in_array_first_matches.py b/tests/acceptance/version_1/testcases/response/body/objects_in_array_first_matches.py new file mode 100644 index 0000000..6a16de2 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/objects_in_array_first_matches.py @@ -0,0 +1,23 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_values(): + data = load_acceptance_test(__file__) + + response = PactResponse(body=[ + { + 'favouriteColor': 'red', + 'favouriteNumber': 2 + }, + { + 'favouriteColor': 'blue', + 'favouriteNumber': 2 + } + ]) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/body/objects_in_array_no_matches.py b/tests/acceptance/version_1/testcases/response/body/objects_in_array_no_matches.py new file mode 100644 index 0000000..09dd4d7 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/objects_in_array_no_matches.py @@ -0,0 +1,23 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_values(): + data = load_acceptance_test(__file__) + + response = PactResponse(body=[ + { + 'favouriteColor': 'blue', + 'favouriteNumber': 4 + }, + { + 'favouriteColor': 'red', + 'favouriteNumber': 2 + } + ]) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/body/objects_in_array_second_matches.py b/tests/acceptance/version_1/testcases/response/body/objects_in_array_second_matches.py new file mode 100644 index 0000000..09dd4d7 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/objects_in_array_second_matches.py @@ -0,0 +1,23 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_values(): + data = load_acceptance_test(__file__) + + response = PactResponse(body=[ + { + 'favouriteColor': 'blue', + 'favouriteNumber': 4 + }, + { + 'favouriteColor': 'red', + 'favouriteNumber': 2 + } + ]) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/body/plain_text_that_matches.py b/tests/acceptance/version_1/testcases/response/body/plain_text_that_matches.py index 17c906c..8df7db8 100644 --- a/tests/acceptance/version_1/testcases/response/body/plain_text_that_matches.py +++ b/tests/acceptance/version_1/testcases/response/body/plain_text_that_matches.py @@ -1,9 +1,11 @@ +import pytest from pact_test.either import Right from pact_test.models.response import PactResponse from pact_test.matchers.response_matcher import match from tests.acceptance.acceptance_test_loader import load_acceptance_test +@pytest.mark.skip(reason="TravisCI error") def test_matching_plain_text(): data = load_acceptance_test(__file__) diff --git a/tests/acceptance/version_1/testcases/response/body/string_found_in_array_when_number_expected.py b/tests/acceptance/version_1/testcases/response/body/string_found_in_array_when_number_expected.py new file mode 100644 index 0000000..7ae8c4e --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/string_found_in_array_when_number_expected.py @@ -0,0 +1,18 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_case(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={ + 'alligator': { + 'favouriteNumbers': [1, '2', 3] + } + }) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/body/unexpected_index_with_not_null_value.py b/tests/acceptance/version_1/testcases/response/body/unexpected_index_with_not_null_value.py new file mode 100644 index 0000000..5f5a730 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/unexpected_index_with_not_null_value.py @@ -0,0 +1,18 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_case(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={ + 'alligator': { + 'favouriteColours': ['red', 'blue', 'taupe'] + } + }) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/body/unexpected_index_with_null_value.py b/tests/acceptance/version_1/testcases/response/body/unexpected_index_with_null_value.py new file mode 100644 index 0000000..1e3b679 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/unexpected_index_with_null_value.py @@ -0,0 +1,18 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_case(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={ + 'alligator': { + 'favouriteColours': ['red', 'blue', None] + } + }) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/body/unexpected_key_with_not_null_value.py b/tests/acceptance/version_1/testcases/response/body/unexpected_key_with_not_null_value.py new file mode 100644 index 0000000..b9c2447 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/unexpected_key_with_not_null_value.py @@ -0,0 +1,19 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_case(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={ + 'alligator': { + 'name': 'Mary', + 'phoneNumber': '12345678' + } + }) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/response/body/unexpected_key_with_null_value.py b/tests/acceptance/version_1/testcases/response/body/unexpected_key_with_null_value.py new file mode 100644 index 0000000..7043959 --- /dev/null +++ b/tests/acceptance/version_1/testcases/response/body/unexpected_key_with_null_value.py @@ -0,0 +1,19 @@ +from pact_test.either import Left +from pact_test.models.response import PactResponse +from pact_test.matchers.response_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_case(): + data = load_acceptance_test(__file__) + + response = PactResponse(body={ + 'alligator': { + 'name': 'Mary', + 'phoneNumber': None + } + }) + interaction = {'response': {'body': data['expected']['body']}} + test_result = match(interaction, response) + + assert type(test_result) is Left From e4100df03667515a082b585b45fdb204167f14d7 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Mon, 28 Aug 2017 11:18:31 +1000 Subject: [PATCH 46/85] Delete .pyc files before running tests in containers --- tests/environments/Dockerfilepy27 | 3 ++- tests/environments/Dockerfilepy33 | 3 ++- tests/environments/Dockerfilepy34 | 3 ++- tests/environments/Dockerfilepy35 | 3 ++- tests/environments/Dockerfilepy36 | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/environments/Dockerfilepy27 b/tests/environments/Dockerfilepy27 index c83f644..a6990d5 100644 --- a/tests/environments/Dockerfilepy27 +++ b/tests/environments/Dockerfilepy27 @@ -4,4 +4,5 @@ RUN mkdir -p /app WORKDIR /app ADD requirements.txt /app RUN pip install -r requirements.txt -ADD . /app \ No newline at end of file +ADD . /app +RUN find . -name '*.pyc' -delete diff --git a/tests/environments/Dockerfilepy33 b/tests/environments/Dockerfilepy33 index b6262ca..6efb6cd 100644 --- a/tests/environments/Dockerfilepy33 +++ b/tests/environments/Dockerfilepy33 @@ -4,4 +4,5 @@ RUN mkdir -p /app WORKDIR /app ADD requirements.txt /app RUN pip install -r requirements.txt -ADD . /app \ No newline at end of file +ADD . /app +RUN find . -name '*.pyc' -delete diff --git a/tests/environments/Dockerfilepy34 b/tests/environments/Dockerfilepy34 index 3dd51dc..b1acbae 100644 --- a/tests/environments/Dockerfilepy34 +++ b/tests/environments/Dockerfilepy34 @@ -4,4 +4,5 @@ RUN mkdir -p /app WORKDIR /app ADD requirements.txt /app RUN pip install -r requirements.txt -ADD . /app \ No newline at end of file +ADD . /app +RUN find . -name '*.pyc' -delete diff --git a/tests/environments/Dockerfilepy35 b/tests/environments/Dockerfilepy35 index bd2a2c8..7f4cd7a 100644 --- a/tests/environments/Dockerfilepy35 +++ b/tests/environments/Dockerfilepy35 @@ -4,4 +4,5 @@ RUN mkdir -p /app WORKDIR /app ADD requirements.txt /app RUN pip install -r requirements.txt -ADD . /app \ No newline at end of file +ADD . /app +RUN find . -name '*.pyc' -delete diff --git a/tests/environments/Dockerfilepy36 b/tests/environments/Dockerfilepy36 index 3a5ee84..5aaaa67 100644 --- a/tests/environments/Dockerfilepy36 +++ b/tests/environments/Dockerfilepy36 @@ -4,4 +4,5 @@ RUN mkdir -p /app WORKDIR /app ADD requirements.txt /app RUN pip install -r requirements.txt -ADD . /app \ No newline at end of file +ADD . /app +RUN find . -name '*.pyc' -delete From c0b450b4c599b3d0142677b4b256771fe124a0c2 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Mon, 28 Aug 2017 11:40:00 +1000 Subject: [PATCH 47/85] Fix for Python 2.7 --- pact_test/matchers/response_matcher.py | 8 ++++++-- .../testcases/response/body/plain_text_that_matches.json | 0 .../testcases/response/body/plain_text_that_matches.py | 2 -- 3 files changed, 6 insertions(+), 4 deletions(-) mode change 100755 => 100644 tests/acceptance/version_1/testcases/response/body/plain_text_that_matches.json diff --git a/pact_test/matchers/response_matcher.py b/pact_test/matchers/response_matcher.py index ac5b5a0..04537c2 100644 --- a/pact_test/matchers/response_matcher.py +++ b/pact_test/matchers/response_matcher.py @@ -36,8 +36,12 @@ def _match_body(interaction, pact_response): if expected is None and actual is None: return Right(interaction) - if type(expected) is str and type(actual) is str and expected == actual: - return Right(interaction) + try: + if (type(expected) is str or type(expected) is unicode) and type(actual) is str and expected == actual: + return Right(interaction) + except NameError: + if type(expected) is str and type(actual) is str and expected == actual: + return Right(interaction) if type(expected) is dict and type(actual) is dict: if _match_dicts(expected, actual): diff --git a/tests/acceptance/version_1/testcases/response/body/plain_text_that_matches.json b/tests/acceptance/version_1/testcases/response/body/plain_text_that_matches.json old mode 100755 new mode 100644 diff --git a/tests/acceptance/version_1/testcases/response/body/plain_text_that_matches.py b/tests/acceptance/version_1/testcases/response/body/plain_text_that_matches.py index 8df7db8..17c906c 100644 --- a/tests/acceptance/version_1/testcases/response/body/plain_text_that_matches.py +++ b/tests/acceptance/version_1/testcases/response/body/plain_text_that_matches.py @@ -1,11 +1,9 @@ -import pytest from pact_test.either import Right from pact_test.models.response import PactResponse from pact_test.matchers.response_matcher import match from tests.acceptance.acceptance_test_loader import load_acceptance_test -@pytest.mark.skip(reason="TravisCI error") def test_matching_plain_text(): data = load_acceptance_test(__file__) From 3f4f5bf53d891ba1dcf2ba2f531cfa38475cdf29 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Mon, 28 Aug 2017 11:45:10 +1000 Subject: [PATCH 48/85] Keys out of order --- pact_test/matchers/response_matcher.py | 2 +- .../testcases/response/body/keys_out_of_order_match.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pact_test/matchers/response_matcher.py b/pact_test/matchers/response_matcher.py index 04537c2..389a757 100644 --- a/pact_test/matchers/response_matcher.py +++ b/pact_test/matchers/response_matcher.py @@ -56,7 +56,7 @@ def _match_dicts(expected, actual): all_keys = set(actual_keys).issubset(set(expected_keys)) all_values = True - for (k1, v1), (k2, v2) in zip(actual.items(), expected.items()): + for (k1, v1), (k2, v2) in zip(sorted(actual.items()), sorted(expected.items())): all_values = all_values and (v1 == v2) return all_keys and all_values diff --git a/tests/acceptance/version_1/testcases/response/body/keys_out_of_order_match.py b/tests/acceptance/version_1/testcases/response/body/keys_out_of_order_match.py index 94d6c77..d7544e0 100644 --- a/tests/acceptance/version_1/testcases/response/body/keys_out_of_order_match.py +++ b/tests/acceptance/version_1/testcases/response/body/keys_out_of_order_match.py @@ -1,11 +1,9 @@ -import pytest from pact_test.either import Right from pact_test.models.response import PactResponse from pact_test.matchers.response_matcher import match from tests.acceptance.acceptance_test_loader import load_acceptance_test -@pytest.mark.skip(reason="requires further investigation") def test_different_values(): data = load_acceptance_test(__file__) From 3f03de29282638d1a0059325930454da9e27f51f Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Mon, 28 Aug 2017 13:08:52 +1000 Subject: [PATCH 49/85] Deeply nested objects --- README.rst | 20 +++++++- pact_test/matchers/response_matcher.py | 46 +++++++++++++------ setup.py | 2 +- .../response/body/deeply_nested_objects.py | 2 - .../unexpected_key_with_not_null_value.py | 4 +- .../body/unexpected_key_with_null_value.py | 4 +- 6 files changed, 55 insertions(+), 23 deletions(-) diff --git a/README.rst b/README.rst index bfb212e..de7a63f 100644 --- a/README.rst +++ b/README.rst @@ -62,6 +62,16 @@ of an hypothetical restaurant service implemented with the most popular Python w There are few things required to setup and run consumer tests. +Installation +~~~~~~~~~~~~ + +Pact Test is distributed through `PyPi `_ so it can be easily included in the +:code:`requirements.txt` file or normally installed with :code:`pip`: + +.. code:: bash + + $ pip install pact-test + Pact Helper ~~~~~~~~~~~ @@ -102,7 +112,7 @@ honouring the pacts, a provider needs to define such states. For example: .. code:: python - @has_pact_with('Restaurant Service') + @honours_pact_with('UberEats') @pact_uri('http://Kalimaha.github.io/src/pacts/pact.json') class UberEats(ServiceConsumerTest): @@ -144,7 +154,7 @@ Setup .. code:: bash - python setup.py install + python3 setup.py install Test ---- @@ -174,3 +184,9 @@ Upload New Version .. code:: bash $ python3 setup.py sdist upload + +With `Python Wheels `_: + +.. code:: bash + $ python3 setup.py sdist bdist_wheel + $ twine upload dist/* diff --git a/pact_test/matchers/response_matcher.py b/pact_test/matchers/response_matcher.py index 389a757..bcf5c87 100644 --- a/pact_test/matchers/response_matcher.py +++ b/pact_test/matchers/response_matcher.py @@ -36,30 +36,48 @@ def _match_body(interaction, pact_response): if expected is None and actual is None: return Right(interaction) - try: - if (type(expected) is str or type(expected) is unicode) and type(actual) is str and expected == actual: - return Right(interaction) - except NameError: - if type(expected) is str and type(actual) is str and expected == actual: - return Right(interaction) + if _is_string(expected) and _is_string(actual) and expected == actual: + return Right(interaction) - if type(expected) is dict and type(actual) is dict: - if _match_dicts(expected, actual): + if type(expected) is dict and type(actual) is dict and _match_dicts_all_keys_and_values(expected, actual): return Right(interaction) return Left(_build_error_message('body', expected, actual)) -def _match_dicts(expected, actual): - expected_keys = expected.keys() - actual_keys = actual.keys() - all_keys = set(actual_keys).issubset(set(expected_keys)) +def _is_string(text): + try: + return True if (type(text) is str or type(text) is unicode) else False + except NameError: + return True if type(text) is str else False + + +def _match_dicts_all_keys_and_values(d1, d2): + d1_keys = d1.keys() + d2_keys = d2.keys() + + _delete_extra_keys(d1, d2) + + all_keys = set(d2_keys).issubset(set(d1_keys)) + all_values = _match_dicts_all_values(d1, d2) + + return all_keys and all_values + +def _match_dicts_all_values(d1, d2): all_values = True - for (k1, v1), (k2, v2) in zip(sorted(actual.items()), sorted(expected.items())): + for (k1, v1), (k2, v2) in zip(sorted(d2.items()), sorted(d1.items())): all_values = all_values and (v1 == v2) + return all_values - return all_keys and all_values + +def _delete_extra_keys(d1, d2): + extra_keys = list(set(d2.keys()) - set(d1.keys())) + for extra_key in extra_keys: + d2.pop(extra_key, None) + for (k1, v1), (k2, v2) in zip(sorted(d1.items()), sorted(d2.items())): + if type(v1) is dict and type(v2) is dict: + _delete_extra_keys(v1, v2) def _to_dict(headers): diff --git a/setup.py b/setup.py index 68334af..35d10e2 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.1.48', + version='0.1.50', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), diff --git a/tests/acceptance/version_1/testcases/response/body/deeply_nested_objects.py b/tests/acceptance/version_1/testcases/response/body/deeply_nested_objects.py index bc393d2..0ad849c 100644 --- a/tests/acceptance/version_1/testcases/response/body/deeply_nested_objects.py +++ b/tests/acceptance/version_1/testcases/response/body/deeply_nested_objects.py @@ -1,11 +1,9 @@ -import pytest from pact_test.either import Right from pact_test.models.response import PactResponse from pact_test.matchers.response_matcher import match from tests.acceptance.acceptance_test_loader import load_acceptance_test -@pytest.mark.skip(reason="requires further investigation") def test_nested_objects(): data = load_acceptance_test(__file__) diff --git a/tests/acceptance/version_1/testcases/response/body/unexpected_key_with_not_null_value.py b/tests/acceptance/version_1/testcases/response/body/unexpected_key_with_not_null_value.py index b9c2447..9e81eb0 100644 --- a/tests/acceptance/version_1/testcases/response/body/unexpected_key_with_not_null_value.py +++ b/tests/acceptance/version_1/testcases/response/body/unexpected_key_with_not_null_value.py @@ -1,4 +1,4 @@ -from pact_test.either import Left +from pact_test.either import Right from pact_test.models.response import PactResponse from pact_test.matchers.response_matcher import match from tests.acceptance.acceptance_test_loader import load_acceptance_test @@ -16,4 +16,4 @@ def test_different_case(): interaction = {'response': {'body': data['expected']['body']}} test_result = match(interaction, response) - assert type(test_result) is Left + assert type(test_result) is Right diff --git a/tests/acceptance/version_1/testcases/response/body/unexpected_key_with_null_value.py b/tests/acceptance/version_1/testcases/response/body/unexpected_key_with_null_value.py index 7043959..3b936de 100644 --- a/tests/acceptance/version_1/testcases/response/body/unexpected_key_with_null_value.py +++ b/tests/acceptance/version_1/testcases/response/body/unexpected_key_with_null_value.py @@ -1,4 +1,4 @@ -from pact_test.either import Left +from pact_test.either import Right from pact_test.models.response import PactResponse from pact_test.matchers.response_matcher import match from tests.acceptance.acceptance_test_loader import load_acceptance_test @@ -16,4 +16,4 @@ def test_different_case(): interaction = {'response': {'body': data['expected']['body']}} test_result = match(interaction, response) - assert type(test_result) is Left + assert type(test_result) is Right From aa212dedfde849a971ffacc8bbcfb61cdbce329a Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Mon, 28 Aug 2017 13:36:23 +1000 Subject: [PATCH 50/85] README --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index de7a63f..bc05f11 100644 --- a/README.rst +++ b/README.rst @@ -188,5 +188,6 @@ Upload New Version With `Python Wheels `_: .. code:: bash + $ python3 setup.py sdist bdist_wheel $ twine upload dist/* From cac3d8b4d37f3026d476d1e9faa2362be16f8f84 Mon Sep 17 00:00:00 2001 From: Guido Barbaglia Date: Wed, 30 Aug 2017 15:47:16 +1000 Subject: [PATCH 51/85] honours_pact_with for ServiceConsumerTest --- pact_test/__init__.py | 2 ++ pact_test/constants/__init__.py | 1 + pact_test/models/service_consumer_test.py | 16 +++++++------- setup.py | 2 +- tests/models/service_consumer_test.py | 22 +++++++++---------- .../test_restaurant_customer.py | 2 +- .../test_restaurant_customer.py | 2 +- tests/runners/service_consumers/test_suite.py | 4 ++-- 8 files changed, 27 insertions(+), 24 deletions(-) diff --git a/pact_test/__init__.py b/pact_test/__init__.py index b83849f..e2e5037 100644 --- a/pact_test/__init__.py +++ b/pact_test/__init__.py @@ -6,6 +6,7 @@ from pact_test.models.service_provider_test import has_pact_with from pact_test.models.service_provider_test import upon_receiving from pact_test.models.service_provider_test import service_consumer +from pact_test.models.service_consumer_test import honours_pact_with from pact_test.models.service_provider_test import will_respond_with from pact_test.models.service_consumer_test import ServiceConsumerTest from pact_test.models.service_provider_test import ServiceProviderTest @@ -20,5 +21,6 @@ upon_receiving = upon_receiving service_consumer = service_consumer will_respond_with = will_respond_with +honours_pact_with = honours_pact_with ServiceConsumerTest = ServiceConsumerTest ServiceProviderTest = ServiceProviderTest diff --git a/pact_test/constants/__init__.py b/pact_test/constants/__init__.py index c02fca4..3adc1cb 100644 --- a/pact_test/constants/__init__.py +++ b/pact_test/constants/__init__.py @@ -12,4 +12,5 @@ MISSING_SETUP = 'Missing "setup" method in "pact_helper.py".' MISSING_HAS_PACT_WITH = 'Missing setup for "has_pact_with" at ' MISSING_TEAR_DOWN = 'Missing "tear_down" method in "pact_helper.py".' +MISSING_HONOURS_PACT_WITH = 'Missing setup for "honours_pact_with" at ' EXTEND_PACT_HELPER = 'Pact Helper class must extend pact_test.PactHelper' diff --git a/pact_test/models/service_consumer_test.py b/pact_test/models/service_consumer_test.py index 6fed724..7b3a397 100644 --- a/pact_test/models/service_consumer_test.py +++ b/pact_test/models/service_consumer_test.py @@ -4,7 +4,7 @@ class ServiceConsumerTest(object): pact_uri = None - has_pact_with = None + honours_pact_with = None @property def states(self): @@ -17,8 +17,8 @@ def is_valid(self): if self.pact_uri is None: msg = MISSING_PACT_URI + __file__ return Left(msg) - if self.has_pact_with is None: - msg = MISSING_HAS_PACT_WITH + __file__ + if self.honours_pact_with is None: + msg = MISSING_HONOURS_PACT_WITH + __file__ return Left(msg) return Right(True) @@ -35,16 +35,16 @@ def set_pact_uri(self, pact_uri_value): self.pact_uri = pact_uri_value -def has_pact_with(has_pact_with_value): +def honours_pact_with(honours_pact_with_value): def wrapper(calling_class): - setattr(calling_class, 'set_has_pact_with', - eval('set_has_pact_with(calling_class, "' + has_pact_with_value + '")')) + setattr(calling_class, 'set_honours_pact_with', + eval('set_honours_pact_with(calling_class, "' + honours_pact_with_value + '")')) return calling_class return wrapper -def set_has_pact_with(self, has_pact_with_value): - self.has_pact_with = has_pact_with_value +def set_honours_pact_with(self, honours_pact_with_value): + self.honours_pact_with = honours_pact_with_value def state(state_value): diff --git a/setup.py b/setup.py index 35d10e2..08276a2 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.1.50', + version='0.2.0', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), diff --git a/tests/models/service_consumer_test.py b/tests/models/service_consumer_test.py index 7117564..03c3621 100644 --- a/tests/models/service_consumer_test.py +++ b/tests/models/service_consumer_test.py @@ -1,6 +1,6 @@ from pact_test import state from pact_test import pact_uri -from pact_test import has_pact_with +from pact_test import honours_pact_with from pact_test import ServiceConsumerTest @@ -18,28 +18,28 @@ class MyTest(ServiceConsumerTest): assert t.pact_uri == 'http://montypython.com/' -def test_default_has_pact_with(): +def test_default_honours_pact_with(): t = ServiceConsumerTest() - assert t.has_pact_with is None + assert t.honours_pact_with is None -def test_has_pact_with_decorator(): - @has_pact_with('Library App') +def test_honours_pact_with_decorator(): + @honours_pact_with('Library App') class MyTest(ServiceConsumerTest): pass t = MyTest() - assert t.has_pact_with == 'Library App' + assert t.honours_pact_with == 'Library App' def test_decorators(): - @has_pact_with('Library App') + @honours_pact_with('Library App') @pact_uri('http://montypython.com/') class MyTest(ServiceConsumerTest): pass t = MyTest() - assert t.has_pact_with == 'Library App' + assert t.honours_pact_with == 'Library App' assert t.pact_uri == 'http://montypython.com/' @@ -58,7 +58,7 @@ def setup(self): def test_missing_pact_uri(): - @has_pact_with('Restaurant Customer') + @honours_pact_with('Restaurant Customer') class MyTest(ServiceConsumerTest): @state('the breakfast is available') def setup(self): @@ -68,12 +68,12 @@ def setup(self): assert MyTest().is_valid().value.startswith(msg) -def test_missing_has_pact_with(): +def test_missing_honours_pact_with(): @pact_uri('http://montypython.com/') class MyTest(ServiceConsumerTest): @state('the breakfast is available') def setup(self): return 42 - msg = 'Missing setup for "has_pact_with"' + msg = 'Missing setup for "honours_pact_with"' assert MyTest().is_valid().value.startswith(msg) diff --git a/tests/resources/service_consumers/test_restaurant_customer.py b/tests/resources/service_consumers/test_restaurant_customer.py index c09c14f..04a6004 100644 --- a/tests/resources/service_consumers/test_restaurant_customer.py +++ b/tests/resources/service_consumers/test_restaurant_customer.py @@ -1,7 +1,7 @@ from pact_test.models.service_consumer_test import * -@has_pact_with('Restaurant') +@honours_pact_with('Restaurant') @pact_uri('tests/resources/pact_files/simple.json') class TestRestaurantCustomer(ServiceConsumerTest): diff --git a/tests/resources/service_consumers_bad_pact/test_restaurant_customer.py b/tests/resources/service_consumers_bad_pact/test_restaurant_customer.py index ecd38ce..9911672 100644 --- a/tests/resources/service_consumers_bad_pact/test_restaurant_customer.py +++ b/tests/resources/service_consumers_bad_pact/test_restaurant_customer.py @@ -1,7 +1,7 @@ from pact_test.models.service_consumer_test import * -@has_pact_with('Restaurant') +@honours_pact_with('Restaurant') @pact_uri('http://google.com/') class TestRestaurantCustomer(ServiceConsumerTest): diff --git a/tests/runners/service_consumers/test_suite.py b/tests/runners/service_consumers/test_suite.py index d3acf3b..59131fd 100644 --- a/tests/runners/service_consumers/test_suite.py +++ b/tests/runners/service_consumers/test_suite.py @@ -107,7 +107,7 @@ def test_collect_tests(): test = tests[0] assert test.pact_uri == 'tests/resources/pact_files/simple.json' - assert test.has_pact_with == 'Restaurant' + assert test.honours_pact_with == 'Restaurant' state = next(test.states) assert state.state == 'the breakfast is available' @@ -121,7 +121,7 @@ def test_invalid_test(): test = module.TestRestaurantCustomer() t = ServiceConsumerTestSuiteRunner(None) - msg = 'Missing setup for "has_pact_with"' + msg = 'Missing setup for "honours_pact_with"' assert t.verify_test(test).value.startswith(msg) From 8ef32a54481dc66a11a121f3009ba28a1a9d2390 Mon Sep 17 00:00:00 2001 From: Guido Barbaglia Date: Thu, 31 Aug 2017 16:30:14 +1000 Subject: [PATCH 52/85] Update version to debug Pyramid --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 08276a2..ecc682d 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.2.0', + version='0.2.2', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), From 8a5407b6a7095a308d4dc306e8f6bab895d1a193 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Fri, 29 Sep 2017 10:35:28 +1000 Subject: [PATCH 53/85] Mock Server and Factory --- pact_test/constants/__init__.py | 2 + pact_test/factories/__init__.py | 0 pact_test/factories/mock_server_factory.py | 19 ++++ pact_test/servers/__init__.py | 0 pact_test/servers/mock_server.py | 70 ++++++++++++++ requirements.txt | 1 + setup.py | 2 +- tests/factories/mock_sever_factory.py | 12 +++ tests/servers/mock_server.py | 105 +++++++++++++++++++++ 9 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 pact_test/factories/__init__.py create mode 100644 pact_test/factories/mock_server_factory.py create mode 100644 pact_test/servers/__init__.py create mode 100644 pact_test/servers/mock_server.py create mode 100644 tests/factories/mock_sever_factory.py create mode 100644 tests/servers/mock_server.py diff --git a/pact_test/constants/__init__.py b/pact_test/constants/__init__.py index 3adc1cb..fbd6eb4 100644 --- a/pact_test/constants/__init__.py +++ b/pact_test/constants/__init__.py @@ -14,3 +14,5 @@ MISSING_TEAR_DOWN = 'Missing "tear_down" method in "pact_helper.py".' MISSING_HONOURS_PACT_WITH = 'Missing setup for "honours_pact_with" at ' EXTEND_PACT_HELPER = 'Pact Helper class must extend pact_test.PactHelper' +MISSING_REQUEST = 'Missing request. Please add @with_request to your method.' +MISSING_RESPONSE = 'Missing response. Please add @will_respond_with to your method' diff --git a/pact_test/factories/__init__.py b/pact_test/factories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pact_test/factories/mock_server_factory.py b/pact_test/factories/mock_server_factory.py new file mode 100644 index 0000000..9f897d8 --- /dev/null +++ b/pact_test/factories/mock_server_factory.py @@ -0,0 +1,19 @@ +from threading import Thread +from werkzeug.serving import make_server +from pact_test.servers.mock_server import MockServer + + +class MockServerFactory(Thread): + + def __init__(self, expected_request, mock_response, base_url='127.0.0.1', port=5000): + Thread.__init__(self) + self.mock_server = MockServer(expected_request, mock_response) + self.server = make_server(base_url, port, self.mock_server.app) + self.context = self.mock_server.app.app_context() + self.context.push() + + def run(self): + self.server.serve_forever() + + def shutdown(self): + self.server.shutdown() diff --git a/pact_test/servers/__init__.py b/pact_test/servers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pact_test/servers/mock_server.py b/pact_test/servers/mock_server.py new file mode 100644 index 0000000..9be0e47 --- /dev/null +++ b/pact_test/servers/mock_server.py @@ -0,0 +1,70 @@ +from flask import Flask +from flask import request +from flask import Response +from pact_test.constants import * +from pact_test.models.request import PactRequest +from pact_test.models.response import PactResponse + + +class MockServer(object): + + def __init__(self, req, res): + if req is None: + raise Exception(MISSING_REQUEST) + + if res is None: + raise Exception(MISSING_RESPONSE) + + self.request = self.build_request(req) + self.response = self.build_response(res) + + self.app = Flask(__name__) + self.app.add_url_rule(self.request.path, view_func=self.spy, methods=[self.request.method, ]) + + def spy(self): + return self.success() if self.is_matching_request() else self.failure() + + def success(self): + return Response( + response=self.response.body, + status=self.response.status, + headers=self.response.headers + ) + + def failure(self): + print('=== === === INSIDE failure === === ===') + return Response( + status=500, + response={ + 'message': 'Request is not matching the expectation', + 'expected': { + 'body': self.request.body, + 'headers': self.request.headers + }, + 'actual': { + 'body': request.data, + 'headers': request.headers + } + } + ) + + def is_matching_request(self): + return True + + @staticmethod + def build_request(user_request): + return PactRequest( + body=user_request.get('body'), + path=user_request.get('path') or '/', + query=user_request.get('query'), + method=user_request.get('method'), + headers=user_request.get('headers') + ) + + @staticmethod + def build_response(user_response): + return PactResponse( + status=user_response.get('status'), + body=user_response.get('body'), + headers=user_response.get('headers') + ) diff --git a/requirements.txt b/requirements.txt index 5620863..0615574 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ pytest-pep8 pytest-mock pytest-sugar pytest-runner +flask diff --git a/setup.py b/setup.py index ecc682d..74ffbfe 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ license='LICENSE', long_description=open('README.rst').read(), description='Python implementation for Pact (http://pact.io/)', - install_requires=['requests'], + install_requires=['requests', 'flask'], setup_requires=['pytest-runner'], tests_require=[ 'pytest>=3.0', diff --git a/tests/factories/mock_sever_factory.py b/tests/factories/mock_sever_factory.py new file mode 100644 index 0000000..306c46c --- /dev/null +++ b/tests/factories/mock_sever_factory.py @@ -0,0 +1,12 @@ +import requests +from pact_test.factories.mock_server_factory import MockServerFactory + + +def test_factory(): + req = {'method': 'get', 'path': '/books/42'} + res = {'status': 200} + server = MockServerFactory(req, res) + server.start() + response = requests.get('http://localhost:5000/books/42') + server.shutdown() + assert response.status_code == 200 diff --git a/tests/servers/mock_server.py b/tests/servers/mock_server.py new file mode 100644 index 0000000..e816373 --- /dev/null +++ b/tests/servers/mock_server.py @@ -0,0 +1,105 @@ +import pytest +from pact_test.constants import * +from pact_test.servers.mock_server import MockServer + + +def test_initialize_request(): + req = { + 'query': '?special_offer', + 'headers': {'Content-Type': 'application/json'}, + 'body': {'id': 42}, + 'path': '/items', + 'method': 'POST' + } + s = MockServer(req, {}) + + assert s.request.__class__.__name__ == 'PactRequest' + assert s.request.method == 'POST' + assert s.request.body == {'id': 42} + assert s.request.path == '/items' + assert s.request.query == '?special_offer' + + +def test_initialize_response(): + res = { + 'status': 201, + 'headers': {'Content-Type': 'application/json'}, + 'body': {'id': 42} + } + s = MockServer({}, res) + + assert s.response.__class__.__name__ == 'PactResponse' + assert s.response.status == 201 + assert s.response.body == {'id': 42} + assert s.response.headers == {'Content-Type': 'application/json'} + + +def test_empty_request(): + req = None + res = {} + + with pytest.raises(Exception) as e: + MockServer(req, res) + + assert str(e.value) == MISSING_REQUEST + + +def test_empty_response(): + req = {} + res = None + + with pytest.raises(Exception) as e: + MockServer(req, res) + + assert str(e.value) == MISSING_RESPONSE + + +def test_flask_app(): + req = {} + res = {} + s = MockServer(req, res) + + assert s.app is not None + assert s.app.__class__.__name__ == 'Flask' + + +def test_endpoint(): + req = {'path': '/test_path'} + s = MockServer(req, {}) + + rules = list(s.app.url_map.iter_rules()) + + assert rules[0].rule == '/test_path' + assert rules[0].endpoint == 'spy' + assert rules[0].methods == {'GET', 'HEAD', 'OPTIONS'} + + +def test_matching_request(): + req = {} + res = { + 'body': {'id': 42}, + 'status': 201, + 'headers': {'Content-Type': 'application/json'} + } + s = MockServer(req, res) + + assert s.spy().response == res['body'] + assert s.spy().status_code == 201 + assert s.spy().headers['Content-Type'] == 'application/json' + + +def test_non_matching_request(): + class FailingMockServer(MockServer): + def is_matching_request(self): + return False + + req = {} + res = {} + s = FailingMockServer(req, res) + expected_message = 'Request is not matching the expectation' + + with s.app.test_request_context(): + assert s.spy().status_code == 500 + assert s.spy().response['message'] == expected_message + assert type(s.spy().response['expected']) is dict + assert type(s.spy().response['actual']) is dict From 87c7ee41aaafa04c1c9c34a9e62a613332d278ff Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Thu, 5 Oct 2017 17:17:40 +1100 Subject: [PATCH 54/85] Mock Server --- pact_test/constants/__init__.py | 3 + pact_test/factories/mock_server_factory.py | 3 + pact_test/models/service_provider_test.py | 13 ++ pact_test/runners/pact_tests_runner.py | 29 +--- .../provider_tests_runner.py | 6 - .../runners/service_providers/state_test.py | 9 ++ .../runners/service_providers/test_suite.py | 55 +++++++ pact_test/servers/mock_server.py | 147 ++++++++++------- pact_test/utils/logger.py | 31 +++- tests/factories/mock_sever_factory.py | 12 -- tests/models/service_provider_test.py | 18 +++ .../missing_service_consumer.py | 11 ++ .../service_providers/simple_test.py | 13 ++ tests/runners/pact_tests_runner.py | 6 +- tests/runners/service_providers/__init__.py | 0 tests/runners/service_providers/test_suite.py | 32 ++++ tests/servers/mock_server.py | 152 ++++++------------ 17 files changed, 332 insertions(+), 208 deletions(-) delete mode 100644 pact_test/runners/service_providers/provider_tests_runner.py create mode 100644 pact_test/runners/service_providers/state_test.py create mode 100644 pact_test/runners/service_providers/test_suite.py delete mode 100644 tests/factories/mock_sever_factory.py create mode 100644 tests/resources/invalid_service_provider/missing_service_consumer.py create mode 100644 tests/resources/service_providers/simple_test.py create mode 100644 tests/runners/service_providers/__init__.py create mode 100644 tests/runners/service_providers/test_suite.py diff --git a/pact_test/constants/__init__.py b/pact_test/constants/__init__.py index fbd6eb4..86af1a2 100644 --- a/pact_test/constants/__init__.py +++ b/pact_test/constants/__init__.py @@ -5,12 +5,15 @@ FAILED = 'FAILED' PROVIDER_STATE = 'providerState' TEST_PARENT = 'ServiceConsumerTest' +PROVIDER_TEST_PARENT = 'ServiceProviderTest' MISSING_STATE = 'Missing implementation for state ' MISSING_PACT_HELPER = 'Missing "pact_helper.py" at "' MISSING_PACT_URI = 'Missing setup for "pact_uri" at ' MISSING_TESTS = 'There are no consumer tests to verify.' +MISSING_PROVIDER_TESTS = 'There are no provider tests to verify.' MISSING_SETUP = 'Missing "setup" method in "pact_helper.py".' MISSING_HAS_PACT_WITH = 'Missing setup for "has_pact_with" at ' +MISSING_SERVICE_CONSUMER = 'Missing setup for "service_consumer" at ' MISSING_TEAR_DOWN = 'Missing "tear_down" method in "pact_helper.py".' MISSING_HONOURS_PACT_WITH = 'Missing setup for "honours_pact_with" at ' EXTEND_PACT_HELPER = 'Pact Helper class must extend pact_test.PactHelper' diff --git a/pact_test/factories/mock_server_factory.py b/pact_test/factories/mock_server_factory.py index 9f897d8..25e7c49 100644 --- a/pact_test/factories/mock_server_factory.py +++ b/pact_test/factories/mock_server_factory.py @@ -17,3 +17,6 @@ def run(self): def shutdown(self): self.server.shutdown() + + def request(self): + return self.mock_server.request diff --git a/pact_test/models/service_provider_test.py b/pact_test/models/service_provider_test.py index 0066d5a..6b0fc71 100644 --- a/pact_test/models/service_provider_test.py +++ b/pact_test/models/service_provider_test.py @@ -1,3 +1,7 @@ +from pact_test.either import * +from pact_test.constants import * + + class ServiceProviderTest(object): service_consumer = None has_pact_with = None @@ -16,6 +20,15 @@ def decorated_methods(self): if check: yield obj + def is_valid(self): + if self.service_consumer is None: + msg = MISSING_SERVICE_CONSUMER + __file__ + return Left(msg) + if self.has_pact_with is None: + msg = MISSING_HAS_PACT_WITH + __file__ + return Left(msg) + return Right(True) + def service_consumer(service_consumer_value): def wrapper(calling_class): diff --git a/pact_test/runners/pact_tests_runner.py b/pact_test/runners/pact_tests_runner.py index 1c8bb93..6f0d800 100644 --- a/pact_test/runners/pact_tests_runner.py +++ b/pact_test/runners/pact_tests_runner.py @@ -1,8 +1,8 @@ -from pact_test.either import * -from pact_test.utils.logger import * from pact_test.config.config_builder import Config +from pact_test.utils.logger import log_consumers_test_results +from pact_test.utils.logger import log_providers_test_results from pact_test.runners.service_consumers.test_suite import ServiceConsumerTestSuiteRunner -from pact_test.runners.service_providers.provider_tests_runner import ProviderTestsRunner +from pact_test.runners.service_providers.test_suite import ServiceProviderTestSuiteRunner def verify(verify_consumers=False, verify_providers=False): @@ -16,26 +16,9 @@ def verify(verify_consumers=False, verify_providers=False): def run_consumer_tests(config): test_results = ServiceConsumerTestSuiteRunner(config).verify() - if type(test_results) is Left: - error(test_results.value) - else: - if type(test_results.value) is Left: - error(test_results.value.value) - else: - for test_result in test_results.value: - print() - info('Test: ' + test_result.value['test']) - for result in test_result.value['results']: - info(' GIVEN ' + result.value['state'] + ' UPON RECEIVING ' + result.value['description']) - info(' status: ' + result.value['status']) - for test_error in result.value['errors']: - error(' expected: ' + str(test_error['expected'])) - error(' actual: ' + str(test_error['actual'])) - error(' message: ' + str(test_error['message'])) - info('') - info('Goodbye!') - print() + log_consumers_test_results(test_results) def run_provider_tests(config): - ProviderTestsRunner(config).verify() + test_results = ServiceProviderTestSuiteRunner(config).verify() + log_providers_test_results(test_results) diff --git a/pact_test/runners/service_providers/provider_tests_runner.py b/pact_test/runners/service_providers/provider_tests_runner.py deleted file mode 100644 index 771fa80..0000000 --- a/pact_test/runners/service_providers/provider_tests_runner.py +++ /dev/null @@ -1,6 +0,0 @@ -class ProviderTestsRunner(object): - def __init__(self, config): - self.config = config - - def verify(self): - pass diff --git a/pact_test/runners/service_providers/state_test.py b/pact_test/runners/service_providers/state_test.py new file mode 100644 index 0000000..7135703 --- /dev/null +++ b/pact_test/runners/service_providers/state_test.py @@ -0,0 +1,9 @@ +from pact_test.factories.mock_server_factory import MockServerFactory + + +def verify_state(decorated_method): + mock_server = MockServerFactory(decorated_method.with_request, decorated_method.will_respond_with) + + mock_server.start() + decorated_method() + mock_server.shutdown() diff --git a/pact_test/runners/service_providers/test_suite.py b/pact_test/runners/service_providers/test_suite.py new file mode 100644 index 0000000..2672edb --- /dev/null +++ b/pact_test/runners/service_providers/test_suite.py @@ -0,0 +1,55 @@ +import os +import imp +import inspect +from pact_test.either import * +from pact_test.constants import * +from pact_test.utils.logger import error +from pact_test.utils.logger import debug + + +class ServiceProviderTestSuiteRunner(object): + def __init__(self, config): + self.config = config + debug(config) + debug(self.config.provider_tests_path) + + def verify(self): + debug('Verify providers: START') + tests = self.collect_tests() + if type(tests) is Right: + debug(str(len(tests.value)) + ' test(s) found.') + for test in tests.value: + test_verification = test.is_valid() + if type(test_verification) is Right: + pass + error('Verify providers: EXIT WITH ERRORS:') + error(test_verification.value) + error('Verify providers: EXIT WITH ERRORS:') + error(tests.value) + return tests + + def collect_tests(self): + root = self.config.provider_tests_path + debug(self.config) + debug(root) + files = list(filter(self.filter_rule, self.all_files())) + files = list(map(lambda f: os.path.join(root, f), files)) + tests = [] + for idx, filename in enumerate(files): + test = imp.load_source('test' + str(idx), filename) + for name, obj in inspect.getmembers(test): + if inspect.isclass(obj) and len(inspect.getmro(obj)) > 2: + test_parent = inspect.getmro(obj)[1].__name__ + if test_parent == PROVIDER_TEST_PARENT: + tests.append(obj()) + + if not files: + return Left(MISSING_PROVIDER_TESTS) + return Right(tests) + + @staticmethod + def filter_rule(filename): + return filename != '__init__.py' and filename.endswith('.py') + + def all_files(self): + return os.listdir(self.config.provider_tests_path) diff --git a/pact_test/servers/mock_server.py b/pact_test/servers/mock_server.py index 9be0e47..b47249d 100644 --- a/pact_test/servers/mock_server.py +++ b/pact_test/servers/mock_server.py @@ -1,70 +1,95 @@ -from flask import Flask -from flask import request -from flask import Response -from pact_test.constants import * -from pact_test.models.request import PactRequest +import json +from pact_test.utils.logger import debug +from threading import Thread from pact_test.models.response import PactResponse +try: + import socketserver as SocketServer + import http.server as SimpleHTTPServer +except ImportError: + import SocketServer + import SimpleHTTPServer -class MockServer(object): +ARCHIVE = [] + + +def build_proxy(mock_response=PactResponse()): + class Proxy(SimpleHTTPServer.SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super(Proxy, self).__init__(*args, **kwargs) + self.mock_response = mock_response + + def do_GET(self): + data = self.read_request_data(self) + self.handle_request('GET', data) + + def do_POST(self): + data = self.read_request_data(self) + self.handle_request('POST', data) - def __init__(self, req, res): - if req is None: - raise Exception(MISSING_REQUEST) - - if res is None: - raise Exception(MISSING_RESPONSE) - - self.request = self.build_request(req) - self.response = self.build_response(res) - - self.app = Flask(__name__) - self.app.add_url_rule(self.request.path, view_func=self.spy, methods=[self.request.method, ]) - - def spy(self): - return self.success() if self.is_matching_request() else self.failure() - - def success(self): - return Response( - response=self.response.body, - status=self.response.status, - headers=self.response.headers - ) - - def failure(self): - print('=== === === INSIDE failure === === ===') - return Response( - status=500, - response={ - 'message': 'Request is not matching the expectation', - 'expected': { - 'body': self.request.body, - 'headers': self.request.headers - }, - 'actual': { - 'body': request.data, - 'headers': request.headers - } + def do_PUT(self): + data = self.read_request_data(self) + self.handle_request('PUT', data) + + def do_DELETE(self): + data = self.read_request_data(self) + self.handle_request('DELETE', data) + + @staticmethod + def read_request_data(other_self): + header_value = other_self.headers['Content-Length'] + data_length = int(header_value) if header_value is not None else None + return other_self.rfile.read(data_length) if data_length is not None else None + + def format_request(self, http_method, data): + return { + 'http_method': http_method, + 'path': self.path, + 'data': json.loads(data.decode('utf-8')) if data is not None else data, + 'headers': list(dict([(key, value)]) for key, value in self.headers.items()) } - ) - def is_matching_request(self): - return True + def handle_request(self, http_method, data): + info = self.format_request(http_method, data) + ARCHIVE.append(info) + self.respond() - @staticmethod - def build_request(user_request): - return PactRequest( - body=user_request.get('body'), - path=user_request.get('path') or '/', - query=user_request.get('query'), - method=user_request.get('method'), - headers=user_request.get('headers') - ) + def respond(self): + self.send_response(int(mock_response.status)) + for header in mock_response.headers: + for key, value in header.items(): + self.send_header(key, value) + self.end_headers() + self.wfile.write(str(mock_response.body).encode()) + + return Proxy + + +class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): + pass + + +class MockServer(object): + + def __init__(self, mock_response=PactResponse(), base_url='0.0.0.0', port=1234): + self.port = port + self.base_url = base_url + self.server = ThreadedTCPServer((self.base_url, self.port), build_proxy(mock_response)) + self.server_thread = Thread(target=self.server.serve_forever) + self.server_thread.daemon = True + global ARCHIVE + ARCHIVE = [] + + def start(self): + self.server_thread.start() + debug('PROXY SERVER LISTENING ON http://' + self.base_url + ':' + str(self.port)) + + def shutdown(self): + debug('SHUTTING DOWN SERVER...') + self.server.shutdown() + self.server.server_close() + debug('SHUTTING DOWN MOCK SERVER... DONE') @staticmethod - def build_response(user_response): - return PactResponse( - status=user_response.get('status'), - body=user_response.get('body'), - headers=user_response.get('headers') - ) + def report(): + return ARCHIVE diff --git a/pact_test/utils/logger.py b/pact_test/utils/logger.py index f18ad71..63b1b31 100644 --- a/pact_test/utils/logger.py +++ b/pact_test/utils/logger.py @@ -1,3 +1,5 @@ +from pact_test.either import * + PREFIX = '[Pact Test for Python] - ' # pragma: no cover @@ -10,5 +12,30 @@ def error(message): # pragma: no cover def debug(message): # pragma: no cover - # print('\033[93m' + PREFIX + str(message) + '\033[0m') # pragma: no cover - pass + print('\033[93m' + PREFIX + str(message) + '\033[0m') # pragma: no cover + + +def log_consumers_test_results(test_results): + if type(test_results) is Left: + error(test_results.value) + else: + if type(test_results.value) is Left: + error(test_results.value.value) + else: + for test_result in test_results.value: + print() + info('Test: ' + test_result.value['test']) + for result in test_result.value['results']: + info(' GIVEN ' + result.value['state'] + ' UPON RECEIVING ' + result.value['description']) + info(' status: ' + result.value['status']) + for test_error in result.value['errors']: + error(' expected: ' + str(test_error['expected'])) + error(' actual: ' + str(test_error['actual'])) + error(' message: ' + str(test_error['message'])) + info('') + info('Goodbye!') + print() + + +def log_providers_test_results(test_results): + print(test_results) diff --git a/tests/factories/mock_sever_factory.py b/tests/factories/mock_sever_factory.py deleted file mode 100644 index 306c46c..0000000 --- a/tests/factories/mock_sever_factory.py +++ /dev/null @@ -1,12 +0,0 @@ -import requests -from pact_test.factories.mock_server_factory import MockServerFactory - - -def test_factory(): - req = {'method': 'get', 'path': '/books/42'} - res = {'status': 200} - server = MockServerFactory(req, res) - server.start() - response = requests.get('http://localhost:5000/books/42') - server.shutdown() - assert response.status_code == 200 diff --git a/tests/models/service_provider_test.py b/tests/models/service_provider_test.py index 7155975..1e6c629 100644 --- a/tests/models/service_provider_test.py +++ b/tests/models/service_provider_test.py @@ -78,3 +78,21 @@ def make_me_breakfast(self): assert False except StopIteration: assert True + + +def test_missing_service_consumer(): + @has_pact_with('Restaurant Customer') + class MyTest(ServiceProviderTest): + pass + + msg = 'Missing setup for "service_consumer"' + assert MyTest().is_valid().value.startswith(msg) + + +def test_missing_has_pact_with(): + @service_consumer('Restaurant Customer') + class MyTest(ServiceProviderTest): + pass + + msg = 'Missing setup for "has_pact_with"' + assert MyTest().is_valid().value.startswith(msg) diff --git a/tests/resources/invalid_service_provider/missing_service_consumer.py b/tests/resources/invalid_service_provider/missing_service_consumer.py new file mode 100644 index 0000000..53b8e24 --- /dev/null +++ b/tests/resources/invalid_service_provider/missing_service_consumer.py @@ -0,0 +1,11 @@ +from pact_test.models.service_provider_test import * + + +@has_pact_with('Books Service') +class SimpleTest(ServiceProviderTest): + @given('a book exists') + @upon_receiving('a request for a book') + @with_request({'method': 'get', 'path': '/books/42'}) + @will_respond_with({'status': 200}) + def test_get_book(self): + pass diff --git a/tests/resources/service_providers/simple_test.py b/tests/resources/service_providers/simple_test.py new file mode 100644 index 0000000..9754d09 --- /dev/null +++ b/tests/resources/service_providers/simple_test.py @@ -0,0 +1,13 @@ +from pact_test.models.service_provider_test import * + + +@service_consumer('Library App') +@has_pact_with('Books Service') +class SimpleTest(ServiceProviderTest): + + @given('a book exists') + @upon_receiving('a request for a book') + @with_request({'method': 'get', 'path': '/books/42'}) + @will_respond_with({'status': 200}) + def test_get_book(self): + pass diff --git a/tests/runners/pact_tests_runner.py b/tests/runners/pact_tests_runner.py index 031238d..759a655 100644 --- a/tests/runners/pact_tests_runner.py +++ b/tests/runners/pact_tests_runner.py @@ -1,3 +1,4 @@ +import pytest from pact_test.runners import pact_tests_runner @@ -9,8 +10,9 @@ def test_consumer_tests(mocker): def test_provider_tests(mocker): mocker.spy(pact_tests_runner, 'run_provider_tests') - pact_tests_runner.verify(verify_providers=True) - assert pact_tests_runner.run_provider_tests.call_count == 1 + with pytest.raises(Exception) as e: + pact_tests_runner.verify(verify_providers=True) + assert pact_tests_runner.run_provider_tests.call_count == 1 def test_default_setup(mocker): diff --git a/tests/runners/service_providers/__init__.py b/tests/runners/service_providers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/runners/service_providers/test_suite.py b/tests/runners/service_providers/test_suite.py new file mode 100644 index 0000000..1ba07ba --- /dev/null +++ b/tests/runners/service_providers/test_suite.py @@ -0,0 +1,32 @@ +import os +import sys +import imp +from pact_test.either import Left +from pact_test.config.config_builder import Config +from pact_test.runners.service_providers.test_suite import ServiceProviderTestSuiteRunner # nopep8 + + +def test_empty_tests_list(monkeypatch): + config = Config() + config.provider_tests_path = os.path.join(os.getcwd(), 'tests', + 'resources', + 'service_providers') + + def empty_list(_): + return [] + monkeypatch.setattr(os, 'listdir', empty_list) + + t = ServiceProviderTestSuiteRunner(config) + msg = 'There are no provider tests to verify.' + assert t.collect_tests().value == msg + + +def test_collect_tests(): + config = Config() + config.provider_tests_path = os.path.join(os.getcwd(), 'tests', + 'resources', + 'service_providers') + t = ServiceProviderTestSuiteRunner(config) + + tests = t.collect_tests().value + assert len(tests) == 1 diff --git a/tests/servers/mock_server.py b/tests/servers/mock_server.py index e816373..6507ccd 100644 --- a/tests/servers/mock_server.py +++ b/tests/servers/mock_server.py @@ -1,105 +1,53 @@ -import pytest -from pact_test.constants import * +import requests from pact_test.servers.mock_server import MockServer -def test_initialize_request(): - req = { - 'query': '?special_offer', - 'headers': {'Content-Type': 'application/json'}, - 'body': {'id': 42}, - 'path': '/items', - 'method': 'POST' - } - s = MockServer(req, {}) - - assert s.request.__class__.__name__ == 'PactRequest' - assert s.request.method == 'POST' - assert s.request.body == {'id': 42} - assert s.request.path == '/items' - assert s.request.query == '?special_offer' - - -def test_initialize_response(): - res = { - 'status': 201, - 'headers': {'Content-Type': 'application/json'}, - 'body': {'id': 42} - } - s = MockServer({}, res) - - assert s.response.__class__.__name__ == 'PactResponse' - assert s.response.status == 201 - assert s.response.body == {'id': 42} - assert s.response.headers == {'Content-Type': 'application/json'} - - -def test_empty_request(): - req = None - res = {} - - with pytest.raises(Exception) as e: - MockServer(req, res) - - assert str(e.value) == MISSING_REQUEST - - -def test_empty_response(): - req = {} - res = None - - with pytest.raises(Exception) as e: - MockServer(req, res) - - assert str(e.value) == MISSING_RESPONSE - - -def test_flask_app(): - req = {} - res = {} - s = MockServer(req, res) - - assert s.app is not None - assert s.app.__class__.__name__ == 'Flask' - - -def test_endpoint(): - req = {'path': '/test_path'} - s = MockServer(req, {}) - - rules = list(s.app.url_map.iter_rules()) - - assert rules[0].rule == '/test_path' - assert rules[0].endpoint == 'spy' - assert rules[0].methods == {'GET', 'HEAD', 'OPTIONS'} - - -def test_matching_request(): - req = {} - res = { - 'body': {'id': 42}, - 'status': 201, - 'headers': {'Content-Type': 'application/json'} - } - s = MockServer(req, res) - - assert s.spy().response == res['body'] - assert s.spy().status_code == 201 - assert s.spy().headers['Content-Type'] == 'application/json' - - -def test_non_matching_request(): - class FailingMockServer(MockServer): - def is_matching_request(self): - return False - - req = {} - res = {} - s = FailingMockServer(req, res) - expected_message = 'Request is not matching the expectation' - - with s.app.test_request_context(): - assert s.spy().status_code == 500 - assert s.spy().response['message'] == expected_message - assert type(s.spy().response['expected']) is dict - assert type(s.spy().response['actual']) is dict +def test_no_requests(): + s = MockServer() + s.start() + s.shutdown() + assert s.report() == [] + + +def test_get_request(): + s = MockServer(port=1235) + s.start() + requests.get('http://localhost:1235/') + s.shutdown() + stored_request = s.report()[0] + assert stored_request['http_method'] == 'GET' + assert stored_request['path'] == '/' + assert stored_request['data'] is None + + +def test_post_request(): + s = MockServer(port=1236) + s.start() + requests.post('http://localhost:1236/', data='{"spam": "eggs"}') + s.shutdown() + stored_request = s.report()[0] + assert stored_request['http_method'] == 'POST' + assert stored_request['path'] == '/' + assert stored_request['data'] == {'spam': 'eggs'} + + +def test_put_request(): + s = MockServer(port=1237) + s.start() + requests.put('http://localhost:1237/', data='{"spam": "eggs"}') + s.shutdown() + stored_request = s.report()[0] + assert stored_request['http_method'] == 'PUT' + assert stored_request['path'] == '/' + assert stored_request['data'] == {'spam': 'eggs'} + + +def test_delete_request(): + s = MockServer(port=1238) + s.start() + requests.delete('http://localhost:1238/', data='{"spam": "eggs"}') + s.shutdown() + stored_request = s.report()[0] + assert stored_request['http_method'] == 'DELETE' + assert stored_request['path'] == '/' + assert stored_request['data'] == {'spam': 'eggs'} From 1709ea6ab710970ba75b3602c248e6303c93c93b Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sun, 15 Oct 2017 18:22:18 +1100 Subject: [PATCH 55/85] Fixed weird Python 2/3 init issue --- .travis.yml | 2 +- pact_test/servers/mock_server.py | 5 ++++- requirements.txt | 1 - setup.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index fd00d86..1262335 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: python sudo: false python: - - 2.7 +# - 2.7 - 3.3 - 3.4 - 3.5 diff --git a/pact_test/servers/mock_server.py b/pact_test/servers/mock_server.py index b47249d..06ad146 100644 --- a/pact_test/servers/mock_server.py +++ b/pact_test/servers/mock_server.py @@ -16,7 +16,10 @@ def build_proxy(mock_response=PactResponse()): class Proxy(SimpleHTTPServer.SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): - super(Proxy, self).__init__(*args, **kwargs) + try: + super(Proxy, self).__init__(*args, **kwargs) + except TypeError: + Proxy.__init__(self, *args, **kwargs) self.mock_response = mock_response def do_GET(self): diff --git a/requirements.txt b/requirements.txt index 0615574..5620863 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,3 @@ pytest-pep8 pytest-mock pytest-sugar pytest-runner -flask diff --git a/setup.py b/setup.py index 74ffbfe..ecc682d 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ license='LICENSE', long_description=open('README.rst').read(), description='Python implementation for Pact (http://pact.io/)', - install_requires=['requests', 'flask'], + install_requires=['requests'], setup_requires=['pytest-runner'], tests_require=[ 'pytest>=3.0', From da0e64266c41ab7c9265aac117cf675c965b5df0 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Wed, 18 Oct 2017 18:40:25 +1100 Subject: [PATCH 56/85] Fixed issue with 2.7 class inheritance --- .travis.yml | 2 +- pact_test/servers/mock_server.py | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1262335..fd00d86 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: python sudo: false python: -# - 2.7 + - 2.7 - 3.3 - 3.4 - 3.5 diff --git a/pact_test/servers/mock_server.py b/pact_test/servers/mock_server.py index 06ad146..de630f4 100644 --- a/pact_test/servers/mock_server.py +++ b/pact_test/servers/mock_server.py @@ -16,10 +16,7 @@ def build_proxy(mock_response=PactResponse()): class Proxy(SimpleHTTPServer.SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): - try: - super(Proxy, self).__init__(*args, **kwargs) - except TypeError: - Proxy.__init__(self, *args, **kwargs) + SimpleHTTPServer.SimpleHTTPRequestHandler.__init__(self, *args, **kwargs) self.mock_response = mock_response def do_GET(self): @@ -40,7 +37,7 @@ def do_DELETE(self): @staticmethod def read_request_data(other_self): - header_value = other_self.headers['Content-Length'] + header_value = other_self.headers.get('Content-Length') data_length = int(header_value) if header_value is not None else None return other_self.rfile.read(data_length) if data_length is not None else None From fc3771d29e5f159b1dcd80263c0e635db487070f Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Thu, 19 Oct 2017 19:21:12 +1100 Subject: [PATCH 57/85] WIP --- .../runners/service_providers/test_suite.py | 23 ++++++++++- .../service_providers/simple_test.py | 9 ++++- .../simple_test_without_requests.py | 13 +++++++ tests/runners/service_providers/test_suite.py | 38 ++++++++++++++++++- 4 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 tests/resources/service_providers/simple_test_without_requests.py diff --git a/pact_test/runners/service_providers/test_suite.py b/pact_test/runners/service_providers/test_suite.py index 2672edb..07f12b6 100644 --- a/pact_test/runners/service_providers/test_suite.py +++ b/pact_test/runners/service_providers/test_suite.py @@ -5,6 +5,8 @@ from pact_test.constants import * from pact_test.utils.logger import error from pact_test.utils.logger import debug +from pact_test.models.response import PactResponse +from pact_test.servers.mock_server import MockServer class ServiceProviderTestSuiteRunner(object): @@ -21,13 +23,32 @@ def verify(self): for test in tests.value: test_verification = test.is_valid() if type(test_verification) is Right: - pass + for decorated_method in test.decorated_methods: + mock_response = self.build_expected_response(decorated_method) + mock_server = MockServer(mock_response=mock_response, port=9999) + mock_server.start() + debug('Server is running') + decorated_method() + mock_server.shutdown() + report = mock_server.report() + if len(report) == 0: + error('Verify providers: EXIT WITH ERRORS:') + error(' No request made for ' + str(decorated_method.__name__)) + return Left('No request made for ' + str(decorated_method.__name__)) error('Verify providers: EXIT WITH ERRORS:') error(test_verification.value) error('Verify providers: EXIT WITH ERRORS:') error(tests.value) return tests + @staticmethod + def build_expected_response(decorated_method): + return PactResponse( + body=decorated_method.will_respond_with.get('body'), + status=decorated_method.will_respond_with.get('status'), + headers=decorated_method.will_respond_with.get('headers') + ) + def collect_tests(self): root = self.config.provider_tests_path debug(self.config) diff --git a/tests/resources/service_providers/simple_test.py b/tests/resources/service_providers/simple_test.py index 9754d09..af720d5 100644 --- a/tests/resources/service_providers/simple_test.py +++ b/tests/resources/service_providers/simple_test.py @@ -7,7 +7,14 @@ class SimpleTest(ServiceProviderTest): @given('a book exists') @upon_receiving('a request for a book') - @with_request({'method': 'get', 'path': '/books/42'}) + @with_request({'method': 'get', 'path': '/books/42/'}) @will_respond_with({'status': 200}) def test_get_book(self): pass + + @given('several books exist') + @upon_receiving('a request for a book') + @with_request({'method': 'get', 'path': '/books/'}) + @will_respond_with({'status': 200}) + def test_get_books(self): + pass diff --git a/tests/resources/service_providers/simple_test_without_requests.py b/tests/resources/service_providers/simple_test_without_requests.py new file mode 100644 index 0000000..dd5e502 --- /dev/null +++ b/tests/resources/service_providers/simple_test_without_requests.py @@ -0,0 +1,13 @@ +from pact_test.models.service_provider_test import * + + +@service_consumer('Library App') +@has_pact_with('Books Service') +class SimpleTest(ServiceProviderTest): + + @given('a book exists') + @upon_receiving('a request for a book') + @with_request({'method': 'get', 'path': '/books/42/'}) + @will_respond_with({'status': 200}) + def test_get_book(self): + pass diff --git a/tests/runners/service_providers/test_suite.py b/tests/runners/service_providers/test_suite.py index 1ba07ba..6144202 100644 --- a/tests/runners/service_providers/test_suite.py +++ b/tests/runners/service_providers/test_suite.py @@ -1,6 +1,7 @@ import os import sys import imp +from pact_test import * from pact_test.either import Left from pact_test.config.config_builder import Config from pact_test.runners.service_providers.test_suite import ServiceProviderTestSuiteRunner # nopep8 @@ -29,4 +30,39 @@ def test_collect_tests(): t = ServiceProviderTestSuiteRunner(config) tests = t.collect_tests().value - assert len(tests) == 1 + assert len(tests) == 2 + + +def test_build_expected_response(): + mock_resp = { + 'status': 418, + 'body': {'spam': 'eggs'}, + 'headers': [{'Spam': 'eggs'}] + } + + class SimpleTest(ServiceProviderTest): + @given('the breakfast menu is available') + @upon_receiving('a request for a breakfast') + @with_request('I don\'t like spam') + @will_respond_with(mock_resp) + def test_get_book(self): + pass + + simple_test = SimpleTest() + method = next(simple_test.decorated_methods) + response = ServiceProviderTestSuiteRunner.build_expected_response(method) + + assert response.status == 418 + assert response.headers == [{'Spam': 'eggs'}] + assert response.body == {'spam': 'eggs'} + + +def test_mock_server(): + config = Config() + config.provider_tests_path = os.path.join(os.getcwd(), 'tests', + 'resources', + 'service_providers') + t = ServiceProviderTestSuiteRunner(config) + + test_result = t.verify() + assert type(test_result) is Left From 35d1680baaed935146a2cfd92abb549b4ad664e6 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sun, 22 Oct 2017 11:02:22 +1100 Subject: [PATCH 58/85] WIP Test Request --- pact_test/factories/__init__.py | 0 pact_test/factories/mock_server_factory.py | 22 ------- .../runners/service_providers/request_test.py | 63 +++++++++++++++++++ .../runners/service_providers/state_test.py | 9 --- .../runners/service_providers/test_suite.py | 24 +++++-- .../service_providers_valid/simple_test.py | 14 +++++ .../runners/service_providers/request_test.py | 46 ++++++++++++++ tests/runners/service_providers/test_suite.py | 22 +++---- 8 files changed, 153 insertions(+), 47 deletions(-) delete mode 100644 pact_test/factories/__init__.py delete mode 100644 pact_test/factories/mock_server_factory.py create mode 100644 pact_test/runners/service_providers/request_test.py delete mode 100644 pact_test/runners/service_providers/state_test.py create mode 100644 tests/resources/service_providers_valid/simple_test.py create mode 100644 tests/runners/service_providers/request_test.py diff --git a/pact_test/factories/__init__.py b/pact_test/factories/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pact_test/factories/mock_server_factory.py b/pact_test/factories/mock_server_factory.py deleted file mode 100644 index 25e7c49..0000000 --- a/pact_test/factories/mock_server_factory.py +++ /dev/null @@ -1,22 +0,0 @@ -from threading import Thread -from werkzeug.serving import make_server -from pact_test.servers.mock_server import MockServer - - -class MockServerFactory(Thread): - - def __init__(self, expected_request, mock_response, base_url='127.0.0.1', port=5000): - Thread.__init__(self) - self.mock_server = MockServer(expected_request, mock_response) - self.server = make_server(base_url, port, self.mock_server.app) - self.context = self.mock_server.app.app_context() - self.context.push() - - def run(self): - self.server.serve_forever() - - def shutdown(self): - self.server.shutdown() - - def request(self): - return self.mock_server.request diff --git a/pact_test/runners/service_providers/request_test.py b/pact_test/runners/service_providers/request_test.py new file mode 100644 index 0000000..7e11c0d --- /dev/null +++ b/pact_test/runners/service_providers/request_test.py @@ -0,0 +1,63 @@ +from pact_test.either import * +from pact_test.utils.logger import debug +from pact_test.models.request import PactRequest +from pact_test.models.response import PactResponse +from pact_test.servers.mock_server import MockServer + + +def verify_request(decorated_method, port=9999): + mock_response = build_expected_response(decorated_method) + expected_request = build_expected_request(decorated_method) + mock_server = MockServer(mock_response=mock_response, port=port) + mock_server.start() + debug('Server is running') + decorated_method() + mock_server.shutdown() + report = mock_server.report() + if len(report) is 0: + return Left('Missing request(s) for "' + + format_message(decorated_method) + '"') + actual_request = build_actual_request(report[0]) + return requests_match(expected_request, actual_request) + + +def requests_match(expected, actual): + if expected.method.upper() != actual.method.upper(): + return Left('HTTP methods do not match. Expected "' + + expected.method.upper() + + '", got "' + actual.method.upper() + '".') + + +def build_expected_response(decorated_method): + return PactResponse( + body=decorated_method.will_respond_with.get('body'), + status=decorated_method.will_respond_with.get('status'), + headers=decorated_method.will_respond_with.get('headers') + ) + + +def build_expected_request(decorated_method): + return PactRequest( + method=decorated_method.with_request.get('method'), + body=decorated_method.with_request.get('body'), + headers=decorated_method.with_request.get('headers'), + path=decorated_method.with_request.get('path'), + query=decorated_method.with_request.get('query') + ) + + +def build_actual_request(request): + return PactRequest( + path=request.get('path'), + query=request.get('query'), + method=request.get('http_method'), + body=request.get('data'), + headers=request.get('headers') + ) + + +def format_message(decorated_method): + return 'given ' + \ + decorated_method.given + \ + ', upon receiving ' + \ + decorated_method.upon_receiving diff --git a/pact_test/runners/service_providers/state_test.py b/pact_test/runners/service_providers/state_test.py deleted file mode 100644 index 7135703..0000000 --- a/pact_test/runners/service_providers/state_test.py +++ /dev/null @@ -1,9 +0,0 @@ -from pact_test.factories.mock_server_factory import MockServerFactory - - -def verify_state(decorated_method): - mock_server = MockServerFactory(decorated_method.with_request, decorated_method.will_respond_with) - - mock_server.start() - decorated_method() - mock_server.shutdown() diff --git a/pact_test/runners/service_providers/test_suite.py b/pact_test/runners/service_providers/test_suite.py index 07f12b6..be942a7 100644 --- a/pact_test/runners/service_providers/test_suite.py +++ b/pact_test/runners/service_providers/test_suite.py @@ -20,10 +20,16 @@ def verify(self): tests = self.collect_tests() if type(tests) is Right: debug(str(len(tests.value)) + ' test(s) found.') + test_results = [] for test in tests.value: + test_result = dict() + test_result['service_consumer'] = test.service_consumer + test_result['has_pact_with'] = test.has_pact_with + test_result['states'] = [] test_verification = test.is_valid() if type(test_verification) is Right: for decorated_method in test.decorated_methods: + state = dict() mock_response = self.build_expected_response(decorated_method) mock_server = MockServer(mock_response=mock_response, port=9999) mock_server.start() @@ -31,10 +37,20 @@ def verify(self): decorated_method() mock_server.shutdown() report = mock_server.report() - if len(report) == 0: - error('Verify providers: EXIT WITH ERRORS:') - error(' No request made for ' + str(decorated_method.__name__)) - return Left('No request made for ' + str(decorated_method.__name__)) + if len(report) > 0: + state['given'] = decorated_method.given + state['upon_receiving'] = decorated_method.upon_receiving + state['expected_request'] = decorated_method.with_request + state['request'] = {'http_method': None, 'body': None, 'headers': None} + state['request']['http_method'] = report[0]['http_method'] + state['request']['body'] = report[0]['data'] + state['request']['headers'] = report[0]['headers'] + test_result['states'].append(state) + test_results.append(test_result) + return Right(test_results) + error('Verify providers: EXIT WITH ERRORS:') + error(' No request made for ' + str(decorated_method.__name__)) + return Left('No request made for ' + str(decorated_method.__name__)) error('Verify providers: EXIT WITH ERRORS:') error(test_verification.value) error('Verify providers: EXIT WITH ERRORS:') diff --git a/tests/resources/service_providers_valid/simple_test.py b/tests/resources/service_providers_valid/simple_test.py new file mode 100644 index 0000000..23e6ae6 --- /dev/null +++ b/tests/resources/service_providers_valid/simple_test.py @@ -0,0 +1,14 @@ +import requests +from pact_test.models.service_provider_test import * + + +@service_consumer('Library App') +@has_pact_with('Books Service') +class SimpleTest(ServiceProviderTest): + + @given('a book exists') + @upon_receiving('a request for a book') + @with_request({'method': 'get', 'path': '/books/42/'}) + @will_respond_with({'status': 200}) + def test_get_book(self): + requests.get('http://localhost:9999/books/42') diff --git a/tests/runners/service_providers/request_test.py b/tests/runners/service_providers/request_test.py new file mode 100644 index 0000000..652d27b --- /dev/null +++ b/tests/runners/service_providers/request_test.py @@ -0,0 +1,46 @@ +import requests +from pact_test.models.service_provider_test import * +from pact_test.runners.service_providers.request_test import verify_request + + +def test_missing_request(): + port = 9998 + + class MyTest(ServiceProviderTest): + @given('spam') + @upon_receiving('eggs') + @with_request({'method': 'get', 'path': '/books/42/'}) + @will_respond_with({'status': 200}) + def test_get_book(self): + pass + + t = MyTest() + decorated_method = next(t.decorated_methods) + test_result = verify_request(decorated_method, port) + expected_error_message = 'Missing request(s) for "given spam, ' \ + 'upon receiving eggs"' + + assert type(test_result) is Left + assert test_result.value == expected_error_message + + +def test_non_matching_http_method(): + port = 9997 + url = 'http://localhost:' + str(port) + '/books/42/' + + class MyTest(ServiceProviderTest): + @given('spam') + @upon_receiving('eggs') + @with_request({'method': 'get', 'path': '/books/42/'}) + @will_respond_with({'status': 200}) + def test_get_book(self): + requests.post(url, data='{}') + + t = MyTest() + decorated_method = next(t.decorated_methods) + test_result = verify_request(decorated_method, port) + expected_error_message = 'HTTP methods do not match. ' \ + 'Expected "GET", got "POST".' + + assert type(test_result) is Left + assert test_result.value == expected_error_message diff --git a/tests/runners/service_providers/test_suite.py b/tests/runners/service_providers/test_suite.py index 6144202..e9f72dc 100644 --- a/tests/runners/service_providers/test_suite.py +++ b/tests/runners/service_providers/test_suite.py @@ -1,8 +1,6 @@ import os -import sys -import imp from pact_test import * -from pact_test.either import Left +from pact_test.either import * from pact_test.config.config_builder import Config from pact_test.runners.service_providers.test_suite import ServiceProviderTestSuiteRunner # nopep8 @@ -57,12 +55,12 @@ def test_get_book(self): assert response.body == {'spam': 'eggs'} -def test_mock_server(): - config = Config() - config.provider_tests_path = os.path.join(os.getcwd(), 'tests', - 'resources', - 'service_providers') - t = ServiceProviderTestSuiteRunner(config) - - test_result = t.verify() - assert type(test_result) is Left +# def test_mock_server(): +# config = Config() +# config.provider_tests_path = os.path.join(os.getcwd(), 'tests', +# 'resources', +# 'service_providers') +# t = ServiceProviderTestSuiteRunner(config) +# +# test_result = t.verify() +# assert type(test_result) is Left From 9e0afb9cb41282519bf4918f8234cb0cb0ed3652 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sun, 22 Oct 2017 11:49:45 +1100 Subject: [PATCH 59/85] WIP --- pact_test/matchers/request_matcher.py | 33 ++++++++ .../runners/service_providers/request_test.py | 12 ++- pact_test/servers/mock_server.py | 7 +- .../runners/service_providers/request_test.py | 82 ++++++++++++++++++- 4 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 pact_test/matchers/request_matcher.py diff --git a/pact_test/matchers/request_matcher.py b/pact_test/matchers/request_matcher.py new file mode 100644 index 0000000..1198c4f --- /dev/null +++ b/pact_test/matchers/request_matcher.py @@ -0,0 +1,33 @@ +from pact_test.either import * +from pact_test.constants import FAILED + + +def match(actual, expected): + return _match_path(actual, expected).concat(_match_query, expected).concat(_match_method, expected) + + +def _match_path(actual, expected): + if actual.path == expected.path: + return Right(actual) + return Left(_build_error_message('path', expected.path, actual.path)) + + +def _match_query(actual, expected): + if actual.query == expected.query: + return Right(actual) + return Left(_build_error_message('query', expected.query, actual.query)) + + +def _match_method(actual, expected): + if actual.method.upper() == expected.method.upper(): + return Right(actual) + return Left(_build_error_message('method', expected.method.upper(), actual.method.upper())) + + +def _build_error_message(section, expected, actual): + return { + 'actual': actual, + 'status': FAILED, + 'expected': expected, + 'message': section.capitalize() + ' is incorrect' + } diff --git a/pact_test/runners/service_providers/request_test.py b/pact_test/runners/service_providers/request_test.py index 7e11c0d..1b492db 100644 --- a/pact_test/runners/service_providers/request_test.py +++ b/pact_test/runners/service_providers/request_test.py @@ -3,6 +3,7 @@ from pact_test.models.request import PactRequest from pact_test.models.response import PactResponse from pact_test.servers.mock_server import MockServer +from pact_test.matchers.request_matcher import match def verify_request(decorated_method, port=9999): @@ -18,7 +19,7 @@ def verify_request(decorated_method, port=9999): return Left('Missing request(s) for "' + format_message(decorated_method) + '"') actual_request = build_actual_request(report[0]) - return requests_match(expected_request, actual_request) + return match(actual_request, expected_request) def requests_match(expected, actual): @@ -26,6 +27,14 @@ def requests_match(expected, actual): return Left('HTTP methods do not match. Expected "' + expected.method.upper() + '", got "' + actual.method.upper() + '".') + if expected.path != actual.path: + return Left('Paths do not match. Expected "' + + expected.path + + '", got "' + actual.path + '".') + if expected.query != actual.query: + return Left('Queries do not match. Expected "' + + expected.query + + '", got "' + actual.query + '".') def build_expected_response(decorated_method): @@ -47,6 +56,7 @@ def build_expected_request(decorated_method): def build_actual_request(request): + print(request.get('headers')) return PactRequest( path=request.get('path'), query=request.get('query'), diff --git a/pact_test/servers/mock_server.py b/pact_test/servers/mock_server.py index de630f4..0ced96d 100644 --- a/pact_test/servers/mock_server.py +++ b/pact_test/servers/mock_server.py @@ -42,9 +42,14 @@ def read_request_data(other_self): return other_self.rfile.read(data_length) if data_length is not None else None def format_request(self, http_method, data): + path_and_query = self.path.split('?') + path = path_and_query[0] + query = '?' + path_and_query[1] if len(path_and_query) == 2 else '' + return { 'http_method': http_method, - 'path': self.path, + 'path': path, + 'query': query, 'data': json.loads(data.decode('utf-8')) if data is not None else data, 'headers': list(dict([(key, value)]) for key, value in self.headers.items()) } diff --git a/tests/runners/service_providers/request_test.py b/tests/runners/service_providers/request_test.py index 652d27b..880cff3 100644 --- a/tests/runners/service_providers/request_test.py +++ b/tests/runners/service_providers/request_test.py @@ -39,8 +39,86 @@ def test_get_book(self): t = MyTest() decorated_method = next(t.decorated_methods) test_result = verify_request(decorated_method, port) - expected_error_message = 'HTTP methods do not match. ' \ - 'Expected "GET", got "POST".' + expected_error_message = { + 'actual': 'POST', + 'expected': 'GET', + 'message': 'Method is incorrect', + 'status': 'FAILED' + } assert type(test_result) is Left assert test_result.value == expected_error_message + + +def test_non_matching_path(): + port = 9996 + url = 'http://localhost:' + str(port) + '/books/4242/' + + class MyTest(ServiceProviderTest): + @given('spam') + @upon_receiving('eggs') + @with_request({'method': 'get', 'path': '/books/42/'}) + @will_respond_with({'status': 200}) + def test_get_book(self): + requests.get(url) + + t = MyTest() + decorated_method = next(t.decorated_methods) + test_result = verify_request(decorated_method, port) + expected_error_message = { + 'actual': '/books/4242/', + 'expected': '/books/42/', + 'message': 'Path is incorrect', + 'status': 'FAILED' + } + + assert type(test_result) is Left + assert test_result.value == expected_error_message + + +def test_non_matching_query(): + port = 9995 + url = 'http://localhost:' + str(port) + '/q?spam=eggs' + + class MyTest(ServiceProviderTest): + @given('spam') + @upon_receiving('eggs') + @with_request({'method': 'get', 'path': '/q', 'query': '?eggs=bacon'}) + @will_respond_with({'status': 200}) + def test_get_book(self): + requests.get(url) + + t = MyTest() + decorated_method = next(t.decorated_methods) + test_result = verify_request(decorated_method, port) + expected_error_message = { + 'actual': '?spam=eggs', + 'expected': '?eggs=bacon', + 'message': 'Query is incorrect', + 'status': 'FAILED' + } + + assert type(test_result) is Left + assert test_result.value == expected_error_message + + +def test_non_matching_headers(): + port = 9994 + url = 'http://localhost:' + str(port) + '/' + headers = [{'Content-Type': 'application/json'}] + + class MyTest(ServiceProviderTest): + @given('spam') + @upon_receiving('eggs') + @with_request({'method': 'get', 'path': '/', 'headers': headers}) + @will_respond_with({'status': 200}) + def test_get_book(self): + requests.get(url, headers={'spam': 'eggs'}) + + t = MyTest() + decorated_method = next(t.decorated_methods) + test_result = verify_request(decorated_method, port) + expected_error_message = {} + + # assert type(test_result) is Left + # assert test_result.value == expected_error_message From 74a132307c38a51ca8e722c1bc6fecdcaad873e9 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sun, 22 Oct 2017 13:48:20 +1100 Subject: [PATCH 60/85] Request matcher --- pact_test/matchers/matcher.py | 62 +++++++++++++++ pact_test/matchers/request_matcher.py | 53 +++++++++---- pact_test/matchers/response_matcher.py | 75 ++----------------- .../runners/service_providers/request_test.py | 27 ++----- .../runners/service_providers/test_suite.py | 43 +---------- pact_test/servers/mock_server.py | 4 +- .../runners/service_providers/request_test.py | 46 +++++++++++- tests/runners/service_providers/test_suite.py | 37 --------- tests/servers/mock_server.py | 16 ++-- 9 files changed, 170 insertions(+), 193 deletions(-) create mode 100644 pact_test/matchers/matcher.py diff --git a/pact_test/matchers/matcher.py b/pact_test/matchers/matcher.py new file mode 100644 index 0000000..d85842b --- /dev/null +++ b/pact_test/matchers/matcher.py @@ -0,0 +1,62 @@ +from pact_test.constants import FAILED + + +def is_subset(expected, actual): + actual_items = actual.items() if actual else {} + expected_items = expected.items() if expected else {} + + stripped_actual_items = map(strip_whitespaces_after_commas, actual_items) + stripped_expected_items = map(strip_whitespaces_after_commas, expected_items) + + return all(item in stripped_actual_items for item in stripped_expected_items) + + +def strip_whitespaces_after_commas(t): + k = t[0] + v = t[1].replace(', ', ',') if type(t[1]) is str else t[1] + + return k, v + + +def build_error_message(section, expected, actual): + return { + 'actual': actual, + 'status': FAILED, + 'expected': expected, + 'message': section.capitalize() + ' is incorrect' + } + + +def is_string(text): + try: + return True if (type(text) is str or type(text) is unicode) else False + except NameError: + return True if type(text) is str else False + + +def match_dicts_all_keys_and_values(d1, d2): + d1_keys = d1.keys() + d2_keys = d2.keys() + + delete_extra_keys(d1, d2) + + all_keys = set(d2_keys).issubset(set(d1_keys)) + all_values = match_dicts_all_values(d1, d2) + + return all_keys and all_values + + +def match_dicts_all_values(d1, d2): + all_values = True + for (k1, v1), (k2, v2) in zip(sorted(d2.items()), sorted(d1.items())): + all_values = all_values and (v1 == v2) + return all_values + + +def delete_extra_keys(d1, d2): + extra_keys = list(set(d2.keys()) - set(d1.keys())) + for extra_key in extra_keys: + d2.pop(extra_key, None) + for (k1, v1), (k2, v2) in zip(sorted(d1.items()), sorted(d2.items())): + if type(v1) is dict and type(v2) is dict: + delete_extra_keys(v1, v2) diff --git a/pact_test/matchers/request_matcher.py b/pact_test/matchers/request_matcher.py index 1198c4f..82d3d23 100644 --- a/pact_test/matchers/request_matcher.py +++ b/pact_test/matchers/request_matcher.py @@ -1,33 +1,58 @@ from pact_test.either import * -from pact_test.constants import FAILED +from pact_test.matchers.matcher import * def match(actual, expected): - return _match_path(actual, expected).concat(_match_query, expected).concat(_match_method, expected) + return _match_path(actual, expected)\ + .concat(_match_query, expected)\ + .concat(_match_method, expected)\ + .concat(_match_headers, expected)\ + .concat(_match_body, expected) + + +def _match_body(actual, expected): + expected_body = expected.body + actual_body = actual.body + + if expected_body is None and actual_body is None: + return Right(actual) + + if is_string(expected_body) and is_string(actual_body) and expected_body == actual_body: + return Right(actual) + + if type(expected_body) is dict \ + and type(actual_body) is dict \ + and match_dicts_all_keys_and_values(expected_body, actual_body): + return Right(actual) + + return Left(build_error_message('body', expected_body, actual_body)) + + +def _match_headers(actual, expected): + actual_dict = dict(pair for d in actual.headers for pair in d.items()) + expected_dict = dict(pair for d in expected.headers for pair in d.items()) + + insensitive_actual = {k.upper(): v for (k, v) in actual_dict.items()} + insensitive_expected = {k.upper(): v for (k, v) in expected_dict.items()} + + if is_subset(insensitive_expected, insensitive_actual): + return Right(actual) + return Left(build_error_message('headers', expected.headers, actual.headers)) def _match_path(actual, expected): if actual.path == expected.path: return Right(actual) - return Left(_build_error_message('path', expected.path, actual.path)) + return Left(build_error_message('path', expected.path, actual.path)) def _match_query(actual, expected): if actual.query == expected.query: return Right(actual) - return Left(_build_error_message('query', expected.query, actual.query)) + return Left(build_error_message('query', expected.query, actual.query)) def _match_method(actual, expected): if actual.method.upper() == expected.method.upper(): return Right(actual) - return Left(_build_error_message('method', expected.method.upper(), actual.method.upper())) - - -def _build_error_message(section, expected, actual): - return { - 'actual': actual, - 'status': FAILED, - 'expected': expected, - 'message': section.capitalize() + ' is incorrect' - } + return Left(build_error_message('method', expected.method.upper(), actual.method.upper())) diff --git a/pact_test/matchers/response_matcher.py b/pact_test/matchers/response_matcher.py index bcf5c87..2b2e873 100644 --- a/pact_test/matchers/response_matcher.py +++ b/pact_test/matchers/response_matcher.py @@ -1,5 +1,5 @@ from pact_test.either import * -from pact_test.constants import FAILED +from pact_test.matchers.matcher import * def match(interaction, pact_response): @@ -14,7 +14,7 @@ def _match_status(interaction, pact_response): if expected is None or actual == expected: return Right(interaction) - return Left(_build_error_message('status', expected, actual)) + return Left(build_error_message('status', expected, actual)) def _match_headers(interaction, pact_response): @@ -24,9 +24,9 @@ def _match_headers(interaction, pact_response): insensitive_expected = {k.upper(): v for (k, v) in expected.items()} insensitive_actual = {k.upper(): v for (k, v) in actual.items()} - if _is_subset(insensitive_expected, insensitive_actual): + if is_subset(insensitive_expected, insensitive_actual): return Right(interaction) - return Left(_build_error_message('headers', expected, actual)) + return Left(build_error_message('headers', expected, actual)) def _match_body(interaction, pact_response): @@ -36,48 +36,13 @@ def _match_body(interaction, pact_response): if expected is None and actual is None: return Right(interaction) - if _is_string(expected) and _is_string(actual) and expected == actual: + if is_string(expected) and is_string(actual) and expected == actual: return Right(interaction) - if type(expected) is dict and type(actual) is dict and _match_dicts_all_keys_and_values(expected, actual): + if type(expected) is dict and type(actual) is dict and match_dicts_all_keys_and_values(expected, actual): return Right(interaction) - return Left(_build_error_message('body', expected, actual)) - - -def _is_string(text): - try: - return True if (type(text) is str or type(text) is unicode) else False - except NameError: - return True if type(text) is str else False - - -def _match_dicts_all_keys_and_values(d1, d2): - d1_keys = d1.keys() - d2_keys = d2.keys() - - _delete_extra_keys(d1, d2) - - all_keys = set(d2_keys).issubset(set(d1_keys)) - all_values = _match_dicts_all_values(d1, d2) - - return all_keys and all_values - - -def _match_dicts_all_values(d1, d2): - all_values = True - for (k1, v1), (k2, v2) in zip(sorted(d2.items()), sorted(d1.items())): - all_values = all_values and (v1 == v2) - return all_values - - -def _delete_extra_keys(d1, d2): - extra_keys = list(set(d2.keys()) - set(d1.keys())) - for extra_key in extra_keys: - d2.pop(extra_key, None) - for (k1, v1), (k2, v2) in zip(sorted(d1.items()), sorted(d2.items())): - if type(v1) is dict and type(v2) is dict: - _delete_extra_keys(v1, v2) + return Left(build_error_message('body', expected, actual)) def _to_dict(headers): @@ -85,29 +50,3 @@ def _to_dict(headers): for h in headers: d[h[0]] = h[1] return d - - -def _build_error_message(section, expected, actual): - return { - 'actual': actual, - 'status': FAILED, - 'expected': expected, - 'message': section.capitalize() + ' is incorrect' - } - - -def _is_subset(expected, actual): - actual_items = actual.items() if actual else {} - expected_items = expected.items() if expected else {} - - stripped_actual_items = map(_strip_whitespaces_after_commas, actual_items) - stripped_expected_items = map(_strip_whitespaces_after_commas, expected_items) - - return all(item in stripped_actual_items for item in stripped_expected_items) - - -def _strip_whitespaces_after_commas(t): - k = t[0] - v = t[1].replace(', ', ',') if type(t[1]) is str else t[1] - - return k, v diff --git a/pact_test/runners/service_providers/request_test.py b/pact_test/runners/service_providers/request_test.py index 1b492db..28db4b1 100644 --- a/pact_test/runners/service_providers/request_test.py +++ b/pact_test/runners/service_providers/request_test.py @@ -1,5 +1,4 @@ from pact_test.either import * -from pact_test.utils.logger import debug from pact_test.models.request import PactRequest from pact_test.models.response import PactResponse from pact_test.servers.mock_server import MockServer @@ -9,34 +8,19 @@ def verify_request(decorated_method, port=9999): mock_response = build_expected_response(decorated_method) expected_request = build_expected_request(decorated_method) + mock_server = MockServer(mock_response=mock_response, port=port) mock_server.start() - debug('Server is running') decorated_method() mock_server.shutdown() report = mock_server.report() + if len(report) is 0: - return Left('Missing request(s) for "' + - format_message(decorated_method) + '"') + return Left('Missing request(s) for "' + format_message(decorated_method) + '"') actual_request = build_actual_request(report[0]) return match(actual_request, expected_request) -def requests_match(expected, actual): - if expected.method.upper() != actual.method.upper(): - return Left('HTTP methods do not match. Expected "' + - expected.method.upper() + - '", got "' + actual.method.upper() + '".') - if expected.path != actual.path: - return Left('Paths do not match. Expected "' + - expected.path + - '", got "' + actual.path + '".') - if expected.query != actual.query: - return Left('Queries do not match. Expected "' + - expected.query + - '", got "' + actual.query + '".') - - def build_expected_response(decorated_method): return PactResponse( body=decorated_method.will_respond_with.get('body'), @@ -56,12 +40,11 @@ def build_expected_request(decorated_method): def build_actual_request(request): - print(request.get('headers')) return PactRequest( path=request.get('path'), query=request.get('query'), - method=request.get('http_method'), - body=request.get('data'), + method=request.get('method'), + body=request.get('body'), headers=request.get('headers') ) diff --git a/pact_test/runners/service_providers/test_suite.py b/pact_test/runners/service_providers/test_suite.py index be942a7..b50e0be 100644 --- a/pact_test/runners/service_providers/test_suite.py +++ b/pact_test/runners/service_providers/test_suite.py @@ -5,66 +5,31 @@ from pact_test.constants import * from pact_test.utils.logger import error from pact_test.utils.logger import debug -from pact_test.models.response import PactResponse -from pact_test.servers.mock_server import MockServer +from pact_test.runners.service_providers.request_test import verify_request class ServiceProviderTestSuiteRunner(object): def __init__(self, config): self.config = config - debug(config) - debug(self.config.provider_tests_path) def verify(self): debug('Verify providers: START') tests = self.collect_tests() if type(tests) is Right: debug(str(len(tests.value)) + ' test(s) found.') - test_results = [] for test in tests.value: - test_result = dict() - test_result['service_consumer'] = test.service_consumer - test_result['has_pact_with'] = test.has_pact_with - test_result['states'] = [] test_verification = test.is_valid() if type(test_verification) is Right: + provider_requests = [] for decorated_method in test.decorated_methods: - state = dict() - mock_response = self.build_expected_response(decorated_method) - mock_server = MockServer(mock_response=mock_response, port=9999) - mock_server.start() - debug('Server is running') - decorated_method() - mock_server.shutdown() - report = mock_server.report() - if len(report) > 0: - state['given'] = decorated_method.given - state['upon_receiving'] = decorated_method.upon_receiving - state['expected_request'] = decorated_method.with_request - state['request'] = {'http_method': None, 'body': None, 'headers': None} - state['request']['http_method'] = report[0]['http_method'] - state['request']['body'] = report[0]['data'] - state['request']['headers'] = report[0]['headers'] - test_result['states'].append(state) - test_results.append(test_result) - return Right(test_results) - error('Verify providers: EXIT WITH ERRORS:') - error(' No request made for ' + str(decorated_method.__name__)) - return Left('No request made for ' + str(decorated_method.__name__)) + provider_requests.append(verify_request(decorated_method)) + return Right(provider_requests) error('Verify providers: EXIT WITH ERRORS:') error(test_verification.value) error('Verify providers: EXIT WITH ERRORS:') error(tests.value) return tests - @staticmethod - def build_expected_response(decorated_method): - return PactResponse( - body=decorated_method.will_respond_with.get('body'), - status=decorated_method.will_respond_with.get('status'), - headers=decorated_method.will_respond_with.get('headers') - ) - def collect_tests(self): root = self.config.provider_tests_path debug(self.config) diff --git a/pact_test/servers/mock_server.py b/pact_test/servers/mock_server.py index 0ced96d..8ebc1ab 100644 --- a/pact_test/servers/mock_server.py +++ b/pact_test/servers/mock_server.py @@ -47,10 +47,10 @@ def format_request(self, http_method, data): query = '?' + path_and_query[1] if len(path_and_query) == 2 else '' return { - 'http_method': http_method, + 'method': http_method, 'path': path, 'query': query, - 'data': json.loads(data.decode('utf-8')) if data is not None else data, + 'body': json.loads(data.decode('utf-8')) if data is not None else data, 'headers': list(dict([(key, value)]) for key, value in self.headers.items()) } diff --git a/tests/runners/service_providers/request_test.py b/tests/runners/service_providers/request_test.py index 880cff3..7f00402 100644 --- a/tests/runners/service_providers/request_test.py +++ b/tests/runners/service_providers/request_test.py @@ -118,7 +118,47 @@ def test_get_book(self): t = MyTest() decorated_method = next(t.decorated_methods) test_result = verify_request(decorated_method, port) - expected_error_message = {} + expected_error_message = { + 'actual': [ + {'Host': 'localhost:9994'}, + {'User-Agent': 'python-requests/2.11.1'}, + {'Accept-Encoding': 'gzip, deflate'}, + {'Accept': '*/*'}, + {'Connection': 'keep-alive'}, + {'spam': 'eggs'}], + 'expected': [ + {'Content-Type': 'application/json'} + ], + 'message': 'Headers is incorrect', + 'status': 'FAILED' + } + + assert type(test_result) is Left + assert test_result.value == expected_error_message + + +def test_non_matching_body(): + port = 9993 + url = 'http://localhost:' + str(port) + '/' + body = '{"spam": "eggs"}' + + class MyTest(ServiceProviderTest): + @given('spam') + @upon_receiving('eggs') + @with_request({'method': 'post', 'path': '/', 'body': body}) + @will_respond_with({'status': 200}) + def test_get_book(self): + requests.post(url, data='{"eggs": "bacon"}') + + t = MyTest() + decorated_method = next(t.decorated_methods) + test_result = verify_request(decorated_method, port) + expected_error_message = { + 'actual': {'eggs': 'bacon'}, + 'expected': '{"spam": "eggs"}', + 'message': 'Body is incorrect', + 'status': 'FAILED' + } - # assert type(test_result) is Left - # assert test_result.value == expected_error_message + assert type(test_result) is Left + assert test_result.value == expected_error_message diff --git a/tests/runners/service_providers/test_suite.py b/tests/runners/service_providers/test_suite.py index e9f72dc..635e54c 100644 --- a/tests/runners/service_providers/test_suite.py +++ b/tests/runners/service_providers/test_suite.py @@ -1,6 +1,4 @@ import os -from pact_test import * -from pact_test.either import * from pact_test.config.config_builder import Config from pact_test.runners.service_providers.test_suite import ServiceProviderTestSuiteRunner # nopep8 @@ -29,38 +27,3 @@ def test_collect_tests(): tests = t.collect_tests().value assert len(tests) == 2 - - -def test_build_expected_response(): - mock_resp = { - 'status': 418, - 'body': {'spam': 'eggs'}, - 'headers': [{'Spam': 'eggs'}] - } - - class SimpleTest(ServiceProviderTest): - @given('the breakfast menu is available') - @upon_receiving('a request for a breakfast') - @with_request('I don\'t like spam') - @will_respond_with(mock_resp) - def test_get_book(self): - pass - - simple_test = SimpleTest() - method = next(simple_test.decorated_methods) - response = ServiceProviderTestSuiteRunner.build_expected_response(method) - - assert response.status == 418 - assert response.headers == [{'Spam': 'eggs'}] - assert response.body == {'spam': 'eggs'} - - -# def test_mock_server(): -# config = Config() -# config.provider_tests_path = os.path.join(os.getcwd(), 'tests', -# 'resources', -# 'service_providers') -# t = ServiceProviderTestSuiteRunner(config) -# -# test_result = t.verify() -# assert type(test_result) is Left diff --git a/tests/servers/mock_server.py b/tests/servers/mock_server.py index 6507ccd..c98bfa1 100644 --- a/tests/servers/mock_server.py +++ b/tests/servers/mock_server.py @@ -15,9 +15,9 @@ def test_get_request(): requests.get('http://localhost:1235/') s.shutdown() stored_request = s.report()[0] - assert stored_request['http_method'] == 'GET' + assert stored_request['method'] == 'GET' assert stored_request['path'] == '/' - assert stored_request['data'] is None + assert stored_request['body'] is None def test_post_request(): @@ -26,9 +26,9 @@ def test_post_request(): requests.post('http://localhost:1236/', data='{"spam": "eggs"}') s.shutdown() stored_request = s.report()[0] - assert stored_request['http_method'] == 'POST' + assert stored_request['method'] == 'POST' assert stored_request['path'] == '/' - assert stored_request['data'] == {'spam': 'eggs'} + assert stored_request['body'] == {'spam': 'eggs'} def test_put_request(): @@ -37,9 +37,9 @@ def test_put_request(): requests.put('http://localhost:1237/', data='{"spam": "eggs"}') s.shutdown() stored_request = s.report()[0] - assert stored_request['http_method'] == 'PUT' + assert stored_request['method'] == 'PUT' assert stored_request['path'] == '/' - assert stored_request['data'] == {'spam': 'eggs'} + assert stored_request['body'] == {'spam': 'eggs'} def test_delete_request(): @@ -48,6 +48,6 @@ def test_delete_request(): requests.delete('http://localhost:1238/', data='{"spam": "eggs"}') s.shutdown() stored_request = s.report()[0] - assert stored_request['http_method'] == 'DELETE' + assert stored_request['method'] == 'DELETE' assert stored_request['path'] == '/' - assert stored_request['data'] == {'spam': 'eggs'} + assert stored_request['body'] == {'spam': 'eggs'} From bdd288899585715d528db8fee46ea476f8df97dd Mon Sep 17 00:00:00 2001 From: Guido Barbaglia Date: Wed, 15 Nov 2017 10:48:38 +1100 Subject: [PATCH 61/85] WIP --- tests/runners/service_providers/request_test.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/tests/runners/service_providers/request_test.py b/tests/runners/service_providers/request_test.py index 7f00402..65ce503 100644 --- a/tests/runners/service_providers/request_test.py +++ b/tests/runners/service_providers/request_test.py @@ -118,23 +118,9 @@ def test_get_book(self): t = MyTest() decorated_method = next(t.decorated_methods) test_result = verify_request(decorated_method, port) - expected_error_message = { - 'actual': [ - {'Host': 'localhost:9994'}, - {'User-Agent': 'python-requests/2.11.1'}, - {'Accept-Encoding': 'gzip, deflate'}, - {'Accept': '*/*'}, - {'Connection': 'keep-alive'}, - {'spam': 'eggs'}], - 'expected': [ - {'Content-Type': 'application/json'} - ], - 'message': 'Headers is incorrect', - 'status': 'FAILED' - } assert type(test_result) is Left - assert test_result.value == expected_error_message + assert test_result.value['status'] == 'FAILED' def test_non_matching_body(): From 3b2f65c767c6184fa9d3d30844fda6ff544b519b Mon Sep 17 00:00:00 2001 From: Guido Barbaglia Date: Wed, 15 Nov 2017 11:25:17 +1100 Subject: [PATCH 62/85] Handling for missing tests folder error --- README.rst | 4 +- .../runners/service_providers/request_test.py | 13 +++- .../runners/service_providers/test_suite.py | 72 +++++++++++++------ pact_test/utils/logger.py | 27 ++++++- setup.py | 2 +- tests/runners/pact_tests_runner.py | 6 +- tests/runners/service_providers/test_suite.py | 10 +++ 7 files changed, 102 insertions(+), 32 deletions(-) diff --git a/README.rst b/README.rst index bc05f11..d2b1392 100644 --- a/README.rst +++ b/README.rst @@ -10,7 +10,7 @@ :target: https://travis-ci.org/Kalimaha/pact-test .. image:: https://coveralls.io/repos/github/Kalimaha/pact-test/badge.svg?branch=development :target: https://coveralls.io/github/Kalimaha/pact-test?branch=development -.. image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg +.. image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg :target: https://saythanks.io/to/Kalimaha Pact Test for Python @@ -58,7 +58,7 @@ of an hypothetical restaurant service implemented with the most popular Python w * Djanjo (*TODO*) * `Flask `_ -* Pyramid (*TODO*) +* `Pyramid `_ There are few things required to setup and run consumer tests. diff --git a/pact_test/runners/service_providers/request_test.py b/pact_test/runners/service_providers/request_test.py index 28db4b1..e261c5e 100644 --- a/pact_test/runners/service_providers/request_test.py +++ b/pact_test/runners/service_providers/request_test.py @@ -1,3 +1,4 @@ +from pact_test.utils.logger import * from pact_test.either import * from pact_test.models.request import PactRequest from pact_test.models.response import PactResponse @@ -18,7 +19,17 @@ def verify_request(decorated_method, port=9999): if len(report) is 0: return Left('Missing request(s) for "' + format_message(decorated_method) + '"') actual_request = build_actual_request(report[0]) - return match(actual_request, expected_request) + matching_result = match(actual_request, expected_request) + + if type(matching_result) is Right: + out = { + 'providerState': decorated_method.given, + 'description': decorated_method.upon_receiving, + 'request': actual_request.__dict__, + 'response': mock_response.__dict__ + } + return Right(out) + return matching_result def build_expected_response(decorated_method): diff --git a/pact_test/runners/service_providers/test_suite.py b/pact_test/runners/service_providers/test_suite.py index b50e0be..e183226 100644 --- a/pact_test/runners/service_providers/test_suite.py +++ b/pact_test/runners/service_providers/test_suite.py @@ -17,41 +17,67 @@ def verify(self): tests = self.collect_tests() if type(tests) is Right: debug(str(len(tests.value)) + ' test(s) found.') + pacts = [] for test in tests.value: test_verification = test.is_valid() if type(test_verification) is Right: - provider_requests = [] - for decorated_method in test.decorated_methods: - provider_requests.append(verify_request(decorated_method)) - return Right(provider_requests) - error('Verify providers: EXIT WITH ERRORS:') - error(test_verification.value) + pact = self.create_pact(test) + if len(pact['interactions']) == 0: + error('Verify providers: EXIT WITH ERRORS:') + return Left('No pact-test methods available in test class') + pacts.append(pact) + else: + error('Verify providers: EXIT WITH ERRORS:') + error(test_verification.value) + return test_verification + return Right(pacts) error('Verify providers: EXIT WITH ERRORS:') error(tests.value) return tests + @staticmethod + def create_pact(test): + interactions = [] + for decorated_method in test.decorated_methods: + interactions.append(verify_request(decorated_method).value) + pact = { + 'interactions': interactions, + 'provider': { + 'name': test.has_pact_with + }, + 'consumer': { + 'name': test.service_consumer + } + } + return pact + def collect_tests(self): root = self.config.provider_tests_path - debug(self.config) - debug(root) - files = list(filter(self.filter_rule, self.all_files())) - files = list(map(lambda f: os.path.join(root, f), files)) - tests = [] - for idx, filename in enumerate(files): - test = imp.load_source('test' + str(idx), filename) - for name, obj in inspect.getmembers(test): - if inspect.isclass(obj) and len(inspect.getmro(obj)) > 2: - test_parent = inspect.getmro(obj)[1].__name__ - if test_parent == PROVIDER_TEST_PARENT: - tests.append(obj()) - - if not files: - return Left(MISSING_PROVIDER_TESTS) - return Right(tests) + debug('Root for Provider Tests: ' + str(root)) + all_files = self.all_files() + if type(all_files) is Right: + files = list(filter(self.filter_rule, all_files.value)) + files = list(map(lambda f: os.path.join(root, f), files)) + tests = [] + for idx, filename in enumerate(files): + test = imp.load_source('test' + str(idx), filename) + for name, obj in inspect.getmembers(test): + if inspect.isclass(obj) and len(inspect.getmro(obj)) > 2: + test_parent = inspect.getmro(obj)[1].__name__ + if test_parent == PROVIDER_TEST_PARENT: + tests.append(obj()) + + if not files: + return Left(MISSING_PROVIDER_TESTS) + return Right(tests) + return all_files @staticmethod def filter_rule(filename): return filename != '__init__.py' and filename.endswith('.py') def all_files(self): - return os.listdir(self.config.provider_tests_path) + try: + return Right(os.listdir(self.config.provider_tests_path)) + except FileNotFoundError as e: + return Left(str(e)) diff --git a/pact_test/utils/logger.py b/pact_test/utils/logger.py index 63b1b31..ab681b7 100644 --- a/pact_test/utils/logger.py +++ b/pact_test/utils/logger.py @@ -38,4 +38,29 @@ def log_consumers_test_results(test_results): def log_providers_test_results(test_results): - print(test_results) + if type(test_results) is Left: + error(test_results.value) + else: + for r in test_results.value: + print() + info('A pact between ' + r['consumer']['name'] + ' and ' + r['provider']['name']) + for i in r['interactions']: + info('') + info(' Given ' + i['providerState'] + ', upon receiving ' + i['description'] + ' from ' + r['consumer']['name'] + ' with:') + info('') + info(' {') + info(' "method": ' + str(i['request']['method']) + ',') + info(' "path": ' + str(i['request']['path']) + ',') + info(' "query": ' + str(i['request']['query']) + ',') + info(' "headers": ' + str(i['request']['headers']) + ',') + info(' "body": ' + str(i['request']['body'])) + info(' }') + info('') + info(' ' + r['provider']['name'] + ' will respond with: ') + info('') + info(' {') + info(' "status": ' + str(i['response']['status']) + ',') + info(' "body": ' + str(i['response']['body']) + ',') + info(' "headers": ' + str(i['response']['headers'])) + info(' }') + info('') diff --git a/setup.py b/setup.py index ecc682d..f9989a1 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.2.2', + version='0.3.15', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), diff --git a/tests/runners/pact_tests_runner.py b/tests/runners/pact_tests_runner.py index 759a655..031238d 100644 --- a/tests/runners/pact_tests_runner.py +++ b/tests/runners/pact_tests_runner.py @@ -1,4 +1,3 @@ -import pytest from pact_test.runners import pact_tests_runner @@ -10,9 +9,8 @@ def test_consumer_tests(mocker): def test_provider_tests(mocker): mocker.spy(pact_tests_runner, 'run_provider_tests') - with pytest.raises(Exception) as e: - pact_tests_runner.verify(verify_providers=True) - assert pact_tests_runner.run_provider_tests.call_count == 1 + pact_tests_runner.verify(verify_providers=True) + assert pact_tests_runner.run_provider_tests.call_count == 1 def test_default_setup(mocker): diff --git a/tests/runners/service_providers/test_suite.py b/tests/runners/service_providers/test_suite.py index 635e54c..9f92deb 100644 --- a/tests/runners/service_providers/test_suite.py +++ b/tests/runners/service_providers/test_suite.py @@ -1,4 +1,5 @@ import os +from pact_test.either import * from pact_test.config.config_builder import Config from pact_test.runners.service_providers.test_suite import ServiceProviderTestSuiteRunner # nopep8 @@ -27,3 +28,12 @@ def test_collect_tests(): tests = t.collect_tests().value assert len(tests) == 2 + + +def test_missing_test_directory(): + config = Config() + config.provider_tests_path = os.path.join(os.getcwd(), 'spam') + t = ServiceProviderTestSuiteRunner(config) + result = t.verify() + assert type(result) is Left + assert result.value == "[Errno 2] No such file or directory: '/app/spam'" From 0e181a4a749115c9fe34e546c6aa0958a1b2c998 Mon Sep 17 00:00:00 2001 From: Guido Barbaglia Date: Wed, 15 Nov 2017 15:55:20 +1100 Subject: [PATCH 63/85] Fix CI --- pact_test/runners/service_providers/test_suite.py | 2 +- tests/runners/service_providers/test_suite.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pact_test/runners/service_providers/test_suite.py b/pact_test/runners/service_providers/test_suite.py index e183226..f096d01 100644 --- a/pact_test/runners/service_providers/test_suite.py +++ b/pact_test/runners/service_providers/test_suite.py @@ -79,5 +79,5 @@ def filter_rule(filename): def all_files(self): try: return Right(os.listdir(self.config.provider_tests_path)) - except FileNotFoundError as e: + except Exception as e: return Left(str(e)) diff --git a/tests/runners/service_providers/test_suite.py b/tests/runners/service_providers/test_suite.py index 9f92deb..cd07270 100644 --- a/tests/runners/service_providers/test_suite.py +++ b/tests/runners/service_providers/test_suite.py @@ -36,4 +36,4 @@ def test_missing_test_directory(): t = ServiceProviderTestSuiteRunner(config) result = t.verify() assert type(result) is Left - assert result.value == "[Errno 2] No such file or directory: '/app/spam'" + assert result.value.startswith("[Errno 2] No such file or directory:") From 7d363179c0f8aa90ac7b2dec14fe45a0adbd10cb Mon Sep 17 00:00:00 2001 From: Guido Barbaglia Date: Wed, 15 Nov 2017 16:32:51 +1100 Subject: [PATCH 64/85] Write Pact files --- pact_test/config/config_builder.py | 1 + pact_test/runners/pact_tests_runner.py | 19 +++++++++++++++++++ setup.py | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/pact_test/config/config_builder.py b/pact_test/config/config_builder.py index 5a0547f..096e9fe 100644 --- a/pact_test/config/config_builder.py +++ b/pact_test/config/config_builder.py @@ -6,6 +6,7 @@ class Config(object): pact_broker_uri = None consumer_tests_path = 'tests/service_consumers' provider_tests_path = 'tests/service_providers' + pacts_path = 'pacts' CONFIGURATION_FILE = '.pact.json' def __init__(self): diff --git a/pact_test/runners/pact_tests_runner.py b/pact_test/runners/pact_tests_runner.py index 6f0d800..534f854 100644 --- a/pact_test/runners/pact_tests_runner.py +++ b/pact_test/runners/pact_tests_runner.py @@ -1,3 +1,6 @@ +import os +import json +from pact_test.utils.logger import * from pact_test.config.config_builder import Config from pact_test.utils.logger import log_consumers_test_results from pact_test.utils.logger import log_providers_test_results @@ -22,3 +25,19 @@ def run_consumer_tests(config): def run_provider_tests(config): test_results = ServiceProviderTestSuiteRunner(config).verify() log_providers_test_results(test_results) + if type(test_results) is Right: + write_pact_files(config, test_results.value) + + +def write_pact_files(config, pacts): + pacts_directory = os.path.join(os.getcwd(), config.pacts_path) + if not os.path.exists(pacts_directory): + info('Creating Pacts directory at: ' + str(pacts_directory)) + os.makedirs(pacts_directory) + + for pact in pacts: + filename = pact['consumer']['name'] + '_' + pact['provider']['name'] + '.json' + filename = filename.replace(' ', '_').lower() + info('Writing pact to: ' + filename) + with open(os.path.join(pacts_directory, filename), 'w+') as file: + file.write(json.dumps(pact, indent=2)) diff --git a/setup.py b/setup.py index f9989a1..82ee420 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.3.15', + version='0.3.25', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), From abb068ab8a9a903b5ffe750ad35cf3876c63f64a Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Thu, 16 Nov 2017 11:55:04 +1100 Subject: [PATCH 65/85] Pact Broker repository --- pact_test/repositories/__init__.py | 0 pact_test/repositories/pact_broker.py | 39 +++++++++++++++++ tests/repositories/pact_broker.py | 62 +++++++++++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 pact_test/repositories/__init__.py create mode 100644 pact_test/repositories/pact_broker.py create mode 100644 tests/repositories/pact_broker.py diff --git a/pact_test/repositories/__init__.py b/pact_test/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pact_test/repositories/pact_broker.py b/pact_test/repositories/pact_broker.py new file mode 100644 index 0000000..dc3890d --- /dev/null +++ b/pact_test/repositories/pact_broker.py @@ -0,0 +1,39 @@ +import json +import requests +from pact_test.either import * + + +PACT_BROKER_URL = 'http://localhost:9292/' + + +def upload_pact(provider_name, consumer_name, pact, base_url=PACT_BROKER_URL): + current_version = get_latest_version(consumer_name) + if type(current_version) is Right: + v = next_version(current_version.value) + try: + url = base_url + 'pacts/provider/' + provider_name + '/consumer/' + consumer_name + '/version/' + v + payload = json.dumps(pact) + headers = {'content-type': 'application/json'} + response = requests.put(url, data=payload, headers=headers) + return Right(response.json()) + except requests.exceptions.ConnectionError as e: + msg = 'Failed to establish a new connection with ' + base_url + return Left(msg) + + +def get_latest_version(consumer_name, base_url=PACT_BROKER_URL): + try: + url = base_url + 'pacticipants/' + consumer_name + '/versions/' + response = requests.get(url) + if response.status_code is not 200: + return Right('1.0.0') + return Right(response.json()['_embedded']['versions'][0]['number']) + except requests.exceptions.ConnectionError as e: + msg = 'Failed to establish a new connection with ' + base_url + return Left(msg) + + +def next_version(current_version='1.0.0'): + versions = current_version.split('.') + next_minor = str(1 + int(versions[-1])) + return '.'.join([versions[0], versions[1], next_minor]) diff --git a/tests/repositories/pact_broker.py b/tests/repositories/pact_broker.py new file mode 100644 index 0000000..50e080e --- /dev/null +++ b/tests/repositories/pact_broker.py @@ -0,0 +1,62 @@ +import requests +from pact_test.either import * +from pact_test.repositories.pact_broker import upload_pact +from pact_test.repositories.pact_broker import next_version +from pact_test.repositories.pact_broker import get_latest_version + + +def test_upload_pact(mocker): + class GetResponse(object): + status_code = 200 + + def json(self): + return {'_embedded': {'versions': [{'number': '1.0.41'}]}} + + class PutResponse(object): + status_code = 200 + + def json(self): + return {} + + mocker.patch.object(requests, 'put', lambda x, **kwargs: PutResponse()) + mocker.patch.object(requests, 'get', lambda x, **kwargs: GetResponse()) + + out = upload_pact('provider', 'consumer', {}) + assert type(out) is Right + + +def test_next_version(): + assert next_version('1.0.41') == '1.0.42' + + +def test_get_latest_version(mocker): + class Response(object): + status_code = 200 + + def json(self): + return {'_embedded': {'versions': [{'number': 42}]}} + + mocker.patch.object(requests, 'get', lambda x, **kwargs: Response()) + + latest_version = get_latest_version('eggs') + assert type(latest_version) is Right + assert latest_version.value == 42 + + +def test_missing_latest_version(mocker): + class Response(object): + status_code = 404 + + mocker.patch.object(requests, 'get', lambda x, **kwargs: Response()) + + latest_version = get_latest_version('eggs') + assert type(latest_version) is Right + assert latest_version.value == '1.0.0' + + +def test_wrong_url(): + latest_version = get_latest_version('eggs', base_url='http://host:9999/') + msg = 'Failed to establish a new connection with http://host:9999/' + + assert type(latest_version) is Left + assert latest_version.value == msg From 839959e0e78115cd4a8dd39aad37d7a1e1ecc9c0 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Thu, 16 Nov 2017 13:10:05 +1100 Subject: [PATCH 66/85] Docker Compose configuration for multiple Python environments --- bin/test | 38 ---------------- docker-compose.yml | 43 +++++++++++++++++++ .../Dockerfilepy27 | 1 - .../Dockerfilepy33 | 1 - .../Dockerfilepy34 | 1 - .../Dockerfilepy35 | 1 - .../Dockerfilepy36 | 1 - 7 files changed, 43 insertions(+), 43 deletions(-) delete mode 100755 bin/test create mode 100644 docker-compose.yml rename {tests/environments => environments}/Dockerfilepy27 (79%) rename {tests/environments => environments}/Dockerfilepy33 (79%) rename {tests/environments => environments}/Dockerfilepy34 (79%) rename {tests/environments => environments}/Dockerfilepy35 (79%) rename {tests/environments => environments}/Dockerfilepy36 (79%) diff --git a/bin/test b/bin/test deleted file mode 100755 index d5c43e5..0000000 --- a/bin/test +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash - - -START=$(date +%s) -ENV=${1:-'py36'} -ENVS=('py27' 'py33' 'py34' 'py35' 'py36') - -run_test () { - echo 'Running tests for '$1'... START' - docker build -t pact-test-$1 -f ./tests/environments/Dockerfile$1 . && \ - docker run pact-test-$1 py.test - echo 'Running tests for '$1'... DONE' -} - -help () { - echo '--------------------------------------------------------' - echo 'Usage: test ' - echo '' - echo 'Available values for : py27, py33, py34, py35, py36' - echo '--------------------------------------------------------' -} - -if [[ "${ENVS[*]}" == *$ENV* ]]; then - run_test $ENV -elif [ "$ENV" == "all" ]; then - for i in "${ENVS[@]}" - do - run_test $i - done -else - help -fi - -END=$(date +%s) -DELTA=$(($END - $START)) - -echo -echo 'Execution completed in' $DELTA 'seconds.' \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..662871a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +version: '2.1' + +services: + + py27: + command: sh -c 'find . -name '*.pyc' -delete && python setup.py test' + volumes: + - .:/app + build: + context: . + dockerfile: '/environments/Dockerfilepy27' + + py33: + command: sh -c 'find . -name '*.pyc' -delete && python setup.py test' + volumes: + - .:/app + build: + context: . + dockerfile: '/environments/Dockerfilepy33' + + py34: + command: sh -c 'find . -name '*.pyc' -delete && python setup.py test' + volumes: + - .:/app + build: + context: . + dockerfile: '/environments/Dockerfilepy34' + + py35: + command: sh -c 'find . -name '*.pyc' -delete && python setup.py test' + volumes: + - .:/app + build: + context: . + dockerfile: '/environments/Dockerfilepy35' + + py36: + command: sh -c 'find . -name '*.pyc' -delete && python setup.py test' + volumes: + - .:/app + build: + context: . + dockerfile: '/environments/Dockerfilepy36' diff --git a/tests/environments/Dockerfilepy27 b/environments/Dockerfilepy27 similarity index 79% rename from tests/environments/Dockerfilepy27 rename to environments/Dockerfilepy27 index a6990d5..133fa58 100644 --- a/tests/environments/Dockerfilepy27 +++ b/environments/Dockerfilepy27 @@ -5,4 +5,3 @@ WORKDIR /app ADD requirements.txt /app RUN pip install -r requirements.txt ADD . /app -RUN find . -name '*.pyc' -delete diff --git a/tests/environments/Dockerfilepy33 b/environments/Dockerfilepy33 similarity index 79% rename from tests/environments/Dockerfilepy33 rename to environments/Dockerfilepy33 index 6efb6cd..cbdae56 100644 --- a/tests/environments/Dockerfilepy33 +++ b/environments/Dockerfilepy33 @@ -5,4 +5,3 @@ WORKDIR /app ADD requirements.txt /app RUN pip install -r requirements.txt ADD . /app -RUN find . -name '*.pyc' -delete diff --git a/tests/environments/Dockerfilepy34 b/environments/Dockerfilepy34 similarity index 79% rename from tests/environments/Dockerfilepy34 rename to environments/Dockerfilepy34 index b1acbae..6d53b41 100644 --- a/tests/environments/Dockerfilepy34 +++ b/environments/Dockerfilepy34 @@ -5,4 +5,3 @@ WORKDIR /app ADD requirements.txt /app RUN pip install -r requirements.txt ADD . /app -RUN find . -name '*.pyc' -delete diff --git a/tests/environments/Dockerfilepy35 b/environments/Dockerfilepy35 similarity index 79% rename from tests/environments/Dockerfilepy35 rename to environments/Dockerfilepy35 index 7f4cd7a..604c073 100644 --- a/tests/environments/Dockerfilepy35 +++ b/environments/Dockerfilepy35 @@ -5,4 +5,3 @@ WORKDIR /app ADD requirements.txt /app RUN pip install -r requirements.txt ADD . /app -RUN find . -name '*.pyc' -delete diff --git a/tests/environments/Dockerfilepy36 b/environments/Dockerfilepy36 similarity index 79% rename from tests/environments/Dockerfilepy36 rename to environments/Dockerfilepy36 index 5aaaa67..010abec 100644 --- a/tests/environments/Dockerfilepy36 +++ b/environments/Dockerfilepy36 @@ -5,4 +5,3 @@ WORKDIR /app ADD requirements.txt /app RUN pip install -r requirements.txt ADD . /app -RUN find . -name '*.pyc' -delete From 8dabef7d668306a024d2521afef34de4a7fa3103 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Thu, 16 Nov 2017 14:02:29 +1100 Subject: [PATCH 67/85] Format headers for Pact Broker submission --- pact_test/repositories/pact_broker.py | 17 +++++++++++++ pact_test/runners/pact_tests_runner.py | 10 ++++++++ tests/repositories/pact_broker.py | 35 ++++++++++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/pact_test/repositories/pact_broker.py b/pact_test/repositories/pact_broker.py index dc3890d..fc58913 100644 --- a/pact_test/repositories/pact_broker.py +++ b/pact_test/repositories/pact_broker.py @@ -7,6 +7,7 @@ def upload_pact(provider_name, consumer_name, pact, base_url=PACT_BROKER_URL): + pact = format_headers(pact) current_version = get_latest_version(consumer_name) if type(current_version) is Right: v = next_version(current_version.value) @@ -37,3 +38,19 @@ def next_version(current_version='1.0.0'): versions = current_version.split('.') next_minor = str(1 + int(versions[-1])) return '.'.join([versions[0], versions[1], next_minor]) + + +def format_headers(pact): + for interaction in pact.get('interactions', []): + req_headers = interaction.get('request').get('headers') + fixed_req_headers = {} + for h in req_headers: + fixed_req_headers.update(h) + interaction['request']['headers'] = fixed_req_headers + + res_headers = interaction.get('response').get('headers') + fixes_req_headers = {} + for h in res_headers: + fixes_req_headers.update(h) + interaction['response']['headers'] = fixes_req_headers + return pact diff --git a/pact_test/runners/pact_tests_runner.py b/pact_test/runners/pact_tests_runner.py index 534f854..e7204bd 100644 --- a/pact_test/runners/pact_tests_runner.py +++ b/pact_test/runners/pact_tests_runner.py @@ -29,6 +29,16 @@ def run_provider_tests(config): write_pact_files(config, test_results.value) +def upload_pacts(config, pacts): + for pact in pacts: + provider = pact['provider']['name'] + consumer = pact['consumer']['name'] + payload = json.dumps(pact) + ack = upload_pact(provider, consumer, payload, base_url=config.pact_broker_uri) + if type(ack) is Left: + error(ack.value) + + def write_pact_files(config, pacts): pacts_directory = os.path.join(os.getcwd(), config.pacts_path) if not os.path.exists(pacts_directory): diff --git a/tests/repositories/pact_broker.py b/tests/repositories/pact_broker.py index 50e080e..a3fc33f 100644 --- a/tests/repositories/pact_broker.py +++ b/tests/repositories/pact_broker.py @@ -2,6 +2,7 @@ from pact_test.either import * from pact_test.repositories.pact_broker import upload_pact from pact_test.repositories.pact_broker import next_version +from pact_test.repositories.pact_broker import format_headers from pact_test.repositories.pact_broker import get_latest_version @@ -60,3 +61,37 @@ def test_wrong_url(): assert type(latest_version) is Left assert latest_version.value == msg + + +def test_format_headers(): + pact = { + "interactions": [ + { + "request": { + "headers": [ + {'spam': 'eggs'}, + {'Content-Type': 'application/json'} + ] + }, + "response": { + "headers": [ + {'spam': 'eggs'}, + {'Content-Type': 'application/json'} + ] + } + } + ] + } + new_pact = format_headers(pact) + expected_request_headers = { + 'spam': 'eggs', + 'Content-Type': 'application/json' + } + expected_response_headers = { + 'spam': 'eggs', + 'Content-Type': 'application/json' + } + request_headers = new_pact['interactions'][0]['request']['headers'] + response_headers = new_pact['interactions'][0]['response']['headers'] + assert request_headers == expected_request_headers + assert response_headers == expected_response_headers From 9e2dc285751bfa2a1fd050826118e22dbbf57d01 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Thu, 16 Nov 2017 15:05:41 +1100 Subject: [PATCH 68/85] Debug logs removed --- pact_test/config/config_builder.py | 2 +- pact_test/repositories/pact_broker.py | 2 ++ pact_test/runners/pact_tests_runner.py | 8 +++++--- pact_test/runners/service_consumers/state_test.py | 5 ----- pact_test/utils/pact_utils.py | 3 --- setup.py | 2 +- 6 files changed, 9 insertions(+), 13 deletions(-) diff --git a/pact_test/config/config_builder.py b/pact_test/config/config_builder.py index 096e9fe..571d783 100644 --- a/pact_test/config/config_builder.py +++ b/pact_test/config/config_builder.py @@ -3,7 +3,7 @@ class Config(object): - pact_broker_uri = None + pact_broker_uri = 'http://localhost:9292/' consumer_tests_path = 'tests/service_consumers' provider_tests_path = 'tests/service_providers' pacts_path = 'pacts' diff --git a/pact_test/repositories/pact_broker.py b/pact_test/repositories/pact_broker.py index fc58913..5f60913 100644 --- a/pact_test/repositories/pact_broker.py +++ b/pact_test/repositories/pact_broker.py @@ -7,6 +7,7 @@ def upload_pact(provider_name, consumer_name, pact, base_url=PACT_BROKER_URL): + print() pact = format_headers(pact) current_version = get_latest_version(consumer_name) if type(current_version) is Right: @@ -20,6 +21,7 @@ def upload_pact(provider_name, consumer_name, pact, base_url=PACT_BROKER_URL): except requests.exceptions.ConnectionError as e: msg = 'Failed to establish a new connection with ' + base_url return Left(msg) + print() def get_latest_version(consumer_name, base_url=PACT_BROKER_URL): diff --git a/pact_test/runners/pact_tests_runner.py b/pact_test/runners/pact_tests_runner.py index e7204bd..f5dd54e 100644 --- a/pact_test/runners/pact_tests_runner.py +++ b/pact_test/runners/pact_tests_runner.py @@ -1,7 +1,9 @@ import os import json -from pact_test.utils.logger import * +from pact_test.either import * +from pact_test.utils.logger import info from pact_test.config.config_builder import Config +from pact_test.repositories.pact_broker import upload_pact from pact_test.utils.logger import log_consumers_test_results from pact_test.utils.logger import log_providers_test_results from pact_test.runners.service_consumers.test_suite import ServiceConsumerTestSuiteRunner @@ -27,14 +29,14 @@ def run_provider_tests(config): log_providers_test_results(test_results) if type(test_results) is Right: write_pact_files(config, test_results.value) + upload_pacts(config, test_results.value) def upload_pacts(config, pacts): for pact in pacts: provider = pact['provider']['name'] consumer = pact['consumer']['name'] - payload = json.dumps(pact) - ack = upload_pact(provider, consumer, payload, base_url=config.pact_broker_uri) + ack = upload_pact(provider, consumer, pact, base_url=config.pact_broker_uri) if type(ack) is Left: error(ack.value) diff --git a/pact_test/runners/service_consumers/state_test.py b/pact_test/runners/service_consumers/state_test.py index c618a4a..450e6ca 100644 --- a/pact_test/runners/service_consumers/state_test.py +++ b/pact_test/runners/service_consumers/state_test.py @@ -1,6 +1,5 @@ from pact_test.either import * from pact_test.constants import * -from pact_test.utils.logger import * from pact_test.matchers.response_matcher import match from pact_test.clients.http_client import execute_interaction_request @@ -8,10 +7,7 @@ def verify_state(interaction, pact_helper, test_instance): state = find_state(interaction, interaction['description'], test_instance) if type(state) is Right: - debug('Verify state: "' + str(state.value.state) + '"') - debug('Setup state for test: START') state.value() - debug('Setup state for test: DONE') output = _execute_request(pact_helper, interaction) if type(output) is Right: response_verification = match(interaction, output.value) @@ -34,7 +30,6 @@ def _build_state_response(state, description, response_verification): return Right(_format_message(state.value.state, description, PASSED, [])) else: errors = [response_verification.value] - debug(response_verification.value) return Left(_format_message(state.value.state, description, FAILED, errors)) diff --git a/pact_test/utils/pact_utils.py b/pact_test/utils/pact_utils.py index 71150d7..12df1ea 100644 --- a/pact_test/utils/pact_utils.py +++ b/pact_test/utils/pact_utils.py @@ -1,7 +1,6 @@ import json import requests from pact_test.either import * -from pact_test.utils.logger import debug def get_pact(location): @@ -11,7 +10,6 @@ def get_pact(location): def __get_pact_from_file(filename): - debug('Get pact from file "' + str(filename) + '"') try: with open(filename) as file_content: return Right(json.loads(file_content.read())) @@ -20,7 +18,6 @@ def __get_pact_from_file(filename): def __get_pact_from_url(url): - debug('Get pact from URL "' + str(url) + '"') try: return Right(requests.get(url).json()) except Exception as e: diff --git a/setup.py b/setup.py index 82ee420..fe35a75 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.3.25', + version='0.3.40', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), From 1fd90e920d95c3fc1f8f321b5c85d4b6dd2c9f3c Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Thu, 16 Nov 2017 15:18:33 +1100 Subject: [PATCH 69/85] Tests fixed --- pact_test/runners/service_consumers/state_test.py | 1 + setup.py | 2 +- tests/config/config_builder.py | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pact_test/runners/service_consumers/state_test.py b/pact_test/runners/service_consumers/state_test.py index 450e6ca..80ca77f 100644 --- a/pact_test/runners/service_consumers/state_test.py +++ b/pact_test/runners/service_consumers/state_test.py @@ -1,5 +1,6 @@ from pact_test.either import * from pact_test.constants import * +from pact_test.utils.logger import error from pact_test.matchers.response_matcher import match from pact_test.clients.http_client import execute_interaction_request diff --git a/setup.py b/setup.py index fe35a75..0d54d56 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.3.40', + version='0.3.41', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), diff --git a/tests/config/config_builder.py b/tests/config/config_builder.py index 30be1cf..19f2210 100644 --- a/tests/config/config_builder.py +++ b/tests/config/config_builder.py @@ -14,7 +14,7 @@ def test_default_provider_tests_path(): def test_default_pact_broker_uri(): config = Config() - assert config.pact_broker_uri is None + assert config.pact_broker_uri == 'http://localhost:9292/' def test_custom_consumer_tests_path(): @@ -25,7 +25,7 @@ def path_to_user_config_file(self): 'consumer_only.json') config = TestConfig() - assert config.pact_broker_uri is None + assert config.pact_broker_uri == 'http://localhost:9292/' assert config.consumer_tests_path == 'mypath/mytests' assert config.provider_tests_path == 'tests/service_providers' @@ -38,7 +38,7 @@ def path_to_user_config_file(self): 'provider_only.json') config = TestConfig() - assert config.pact_broker_uri is None + assert config.pact_broker_uri == 'http://localhost:9292/' assert config.provider_tests_path == 'mypath/mytests' assert config.consumer_tests_path == 'tests/service_consumers' From 62f6943da3e298792dec662124d9649aa4228d30 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Fri, 17 Nov 2017 10:54:55 +1100 Subject: [PATCH 70/85] Fixed address already in use issue --- pact_test/matchers/matcher.py | 1 + pact_test/matchers/request_matcher.py | 15 ++++++- pact_test/runners/pact_tests_runner.py | 8 ++-- .../runners/service_providers/request_test.py | 9 +++- .../runners/service_providers/test_suite.py | 2 + pact_test/servers/mock_server.py | 5 ++- pact_test/utils/logger.py | 45 +++++++++++-------- setup.py | 2 +- .../runners/service_providers/request_test.py | 16 +++++-- 9 files changed, 73 insertions(+), 30 deletions(-) diff --git a/pact_test/matchers/matcher.py b/pact_test/matchers/matcher.py index d85842b..0acaadd 100644 --- a/pact_test/matchers/matcher.py +++ b/pact_test/matchers/matcher.py @@ -1,3 +1,4 @@ +from pact_test.utils.logger import * from pact_test.constants import FAILED diff --git a/pact_test/matchers/request_matcher.py b/pact_test/matchers/request_matcher.py index 82d3d23..26bb54d 100644 --- a/pact_test/matchers/request_matcher.py +++ b/pact_test/matchers/request_matcher.py @@ -1,3 +1,4 @@ +from pact_test.utils.logger import * from pact_test.either import * from pact_test.matchers.matcher import * @@ -22,12 +23,24 @@ def _match_body(actual, expected): if type(expected_body) is dict \ and type(actual_body) is dict \ - and match_dicts_all_keys_and_values(expected_body, actual_body): + and _match_dicts_all_keys_and_values(expected_body.copy(), actual_body.copy()): return Right(actual) return Left(build_error_message('body', expected_body, actual_body)) +def _match_dicts_all_keys_and_values(d1, d2): + d1_keys = d1.keys() + d2_keys = d2.keys() + + # delete_extra_keys(d1, d2) + + all_keys = set(d2_keys).issubset(set(d1_keys)) + all_values = match_dicts_all_values(d1, d2) + + return all_keys and all_values + + def _match_headers(actual, expected): actual_dict = dict(pair for d in actual.headers for pair in d.items()) expected_dict = dict(pair for d in expected.headers for pair in d.items()) diff --git a/pact_test/runners/pact_tests_runner.py b/pact_test/runners/pact_tests_runner.py index f5dd54e..12e229b 100644 --- a/pact_test/runners/pact_tests_runner.py +++ b/pact_test/runners/pact_tests_runner.py @@ -36,9 +36,11 @@ def upload_pacts(config, pacts): for pact in pacts: provider = pact['provider']['name'] consumer = pact['consumer']['name'] - ack = upload_pact(provider, consumer, pact, base_url=config.pact_broker_uri) - if type(ack) is Left: - error(ack.value) + is_uploadable = all(interaction['status'] == 'PASSED' for interaction in pact['interactions']) + if is_uploadable: + ack = upload_pact(provider, consumer, pact, base_url=config.pact_broker_uri) + if type(ack) is Left: + error(ack.value) def write_pact_files(config, pacts): diff --git a/pact_test/runners/service_providers/request_test.py b/pact_test/runners/service_providers/request_test.py index e261c5e..fe71627 100644 --- a/pact_test/runners/service_providers/request_test.py +++ b/pact_test/runners/service_providers/request_test.py @@ -23,13 +23,20 @@ def verify_request(decorated_method, port=9999): if type(matching_result) is Right: out = { + 'status': 'PASSED', 'providerState': decorated_method.given, 'description': decorated_method.upon_receiving, 'request': actual_request.__dict__, 'response': mock_response.__dict__ } return Right(out) - return matching_result + else: + reason = matching_result.value.copy() + reason.update({ + 'providerState': decorated_method.given, + 'description': decorated_method.upon_receiving + }) + return Left(reason) def build_expected_response(decorated_method): diff --git a/pact_test/runners/service_providers/test_suite.py b/pact_test/runners/service_providers/test_suite.py index f096d01..cc93fc9 100644 --- a/pact_test/runners/service_providers/test_suite.py +++ b/pact_test/runners/service_providers/test_suite.py @@ -19,6 +19,7 @@ def verify(self): debug(str(len(tests.value)) + ' test(s) found.') pacts = [] for test in tests.value: + debug('Start: ' + test.service_consumer + ' has Pact with ' + test.has_pact_with) test_verification = test.is_valid() if type(test_verification) is Right: pact = self.create_pact(test) @@ -39,6 +40,7 @@ def verify(self): def create_pact(test): interactions = [] for decorated_method in test.decorated_methods: + debug(' Verify interaction: ' + decorated_method.__name__) interactions.append(verify_request(decorated_method).value) pact = { 'interactions': interactions, diff --git a/pact_test/servers/mock_server.py b/pact_test/servers/mock_server.py index 8ebc1ab..56a97e4 100644 --- a/pact_test/servers/mock_server.py +++ b/pact_test/servers/mock_server.py @@ -71,7 +71,9 @@ def respond(self): class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): - pass + def __init__(self, server_address, RequestHandlerClass): + self.allow_reuse_address = True + SocketServer.TCPServer.__init__(self, server_address, RequestHandlerClass) class MockServer(object): @@ -86,6 +88,7 @@ def __init__(self, mock_response=PactResponse(), base_url='0.0.0.0', port=1234): ARCHIVE = [] def start(self): + debug('STARTING PROXY SERVER...') self.server_thread.start() debug('PROXY SERVER LISTENING ON http://' + self.base_url + ':' + str(self.port)) diff --git a/pact_test/utils/logger.py b/pact_test/utils/logger.py index ab681b7..3a32a5d 100644 --- a/pact_test/utils/logger.py +++ b/pact_test/utils/logger.py @@ -45,22 +45,29 @@ def log_providers_test_results(test_results): print() info('A pact between ' + r['consumer']['name'] + ' and ' + r['provider']['name']) for i in r['interactions']: - info('') - info(' Given ' + i['providerState'] + ', upon receiving ' + i['description'] + ' from ' + r['consumer']['name'] + ' with:') - info('') - info(' {') - info(' "method": ' + str(i['request']['method']) + ',') - info(' "path": ' + str(i['request']['path']) + ',') - info(' "query": ' + str(i['request']['query']) + ',') - info(' "headers": ' + str(i['request']['headers']) + ',') - info(' "body": ' + str(i['request']['body'])) - info(' }') - info('') - info(' ' + r['provider']['name'] + ' will respond with: ') - info('') - info(' {') - info(' "status": ' + str(i['response']['status']) + ',') - info(' "body": ' + str(i['response']['body']) + ',') - info(' "headers": ' + str(i['response']['headers'])) - info(' }') - info('') + if i['status'] == 'FAILED': + error(' Given ' + i['providerState'] + ', upon receiving ' + i['description'] + ' from ' + r['consumer']['name']) + error(' Status: ' + i['status']) + error(' Message: ' + i['message']) + error(' Expected: ' + str(i['expected'])) + error(' Actual: ' + str(i['actual'])) + else: + info('') + info(' Given ' + i['providerState'] + ', upon receiving ' + i['description'] + ' from ' + r['consumer']['name'] + ' with:') + info('') + info(' {') + info(' "method": ' + str(i['request']['method']) + ',') + info(' "path": ' + str(i['request']['path']) + ',') + info(' "query": ' + str(i['request']['query']) + ',') + info(' "headers": ' + str(i['request']['headers']) + ',') + info(' "body": ' + str(i['request']['body'])) + info(' }') + info('') + info(' ' + r['provider']['name'] + ' will respond with: ') + info('') + info(' {') + info(' "status": ' + str(i['response']['status']) + ',') + info(' "body": ' + str(i['response']['body']) + ',') + info(' "headers": ' + str(i['response']['headers'])) + info(' }') + info('') diff --git a/setup.py b/setup.py index 0d54d56..ca1e60b 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.3.41', + version='0.3.65', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), diff --git a/tests/runners/service_providers/request_test.py b/tests/runners/service_providers/request_test.py index 65ce503..26efa05 100644 --- a/tests/runners/service_providers/request_test.py +++ b/tests/runners/service_providers/request_test.py @@ -43,7 +43,9 @@ def test_get_book(self): 'actual': 'POST', 'expected': 'GET', 'message': 'Method is incorrect', - 'status': 'FAILED' + 'status': 'FAILED', + 'description': 'eggs', + 'providerState': 'spam' } assert type(test_result) is Left @@ -69,7 +71,9 @@ def test_get_book(self): 'actual': '/books/4242/', 'expected': '/books/42/', 'message': 'Path is incorrect', - 'status': 'FAILED' + 'status': 'FAILED', + 'description': 'eggs', + 'providerState': 'spam' } assert type(test_result) is Left @@ -95,7 +99,9 @@ def test_get_book(self): 'actual': '?spam=eggs', 'expected': '?eggs=bacon', 'message': 'Query is incorrect', - 'status': 'FAILED' + 'status': 'FAILED', + 'description': 'eggs', + 'providerState': 'spam' } assert type(test_result) is Left @@ -143,7 +149,9 @@ def test_get_book(self): 'actual': {'eggs': 'bacon'}, 'expected': '{"spam": "eggs"}', 'message': 'Body is incorrect', - 'status': 'FAILED' + 'status': 'FAILED', + 'description': 'eggs', + 'providerState': 'spam' } assert type(test_result) is Left From 6fac0ddb93c6c4d849d2e7d7ad429aa3f75a35a9 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Fri, 17 Nov 2017 11:10:51 +1100 Subject: [PATCH 71/85] Handling of missing request error --- pact_test/runners/service_providers/request_test.py | 11 ++++++++++- pact_test/servers/mock_server.py | 4 ---- pact_test/utils/logger.py | 4 ++-- setup.py | 2 +- tests/runners/service_providers/request_test.py | 4 +--- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/pact_test/runners/service_providers/request_test.py b/pact_test/runners/service_providers/request_test.py index fe71627..cd86bad 100644 --- a/pact_test/runners/service_providers/request_test.py +++ b/pact_test/runners/service_providers/request_test.py @@ -17,7 +17,16 @@ def verify_request(decorated_method, port=9999): report = mock_server.report() if len(report) is 0: - return Left('Missing request(s) for "' + format_message(decorated_method) + '"') + out = { + 'status': 'FAILED', + 'providerState': decorated_method.given, + 'description': decorated_method.upon_receiving, + 'message': 'Missing request(s) for "' + format_message(decorated_method) + '"', + 'expected': expected_request.__dict__, + 'actual': None + } + return Left(out) + actual_request = build_actual_request(report[0]) matching_result = match(actual_request, expected_request) diff --git a/pact_test/servers/mock_server.py b/pact_test/servers/mock_server.py index 56a97e4..8f5a17e 100644 --- a/pact_test/servers/mock_server.py +++ b/pact_test/servers/mock_server.py @@ -88,15 +88,11 @@ def __init__(self, mock_response=PactResponse(), base_url='0.0.0.0', port=1234): ARCHIVE = [] def start(self): - debug('STARTING PROXY SERVER...') self.server_thread.start() - debug('PROXY SERVER LISTENING ON http://' + self.base_url + ':' + str(self.port)) def shutdown(self): - debug('SHUTTING DOWN SERVER...') self.server.shutdown() self.server.server_close() - debug('SHUTTING DOWN MOCK SERVER... DONE') @staticmethod def report(): diff --git a/pact_test/utils/logger.py b/pact_test/utils/logger.py index 3a32a5d..6d3b950 100644 --- a/pact_test/utils/logger.py +++ b/pact_test/utils/logger.py @@ -49,8 +49,8 @@ def log_providers_test_results(test_results): error(' Given ' + i['providerState'] + ', upon receiving ' + i['description'] + ' from ' + r['consumer']['name']) error(' Status: ' + i['status']) error(' Message: ' + i['message']) - error(' Expected: ' + str(i['expected'])) - error(' Actual: ' + str(i['actual'])) + error(' Expected: ' + str(i.get('expected'))) + error(' Actual: ' + str(i.get('actual'))) else: info('') info(' Given ' + i['providerState'] + ', upon receiving ' + i['description'] + ' from ' + r['consumer']['name'] + ' with:') diff --git a/setup.py b/setup.py index ca1e60b..9ac7361 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.3.65', + version='0.3.66', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), diff --git a/tests/runners/service_providers/request_test.py b/tests/runners/service_providers/request_test.py index 26efa05..cb4ce3c 100644 --- a/tests/runners/service_providers/request_test.py +++ b/tests/runners/service_providers/request_test.py @@ -17,11 +17,9 @@ def test_get_book(self): t = MyTest() decorated_method = next(t.decorated_methods) test_result = verify_request(decorated_method, port) - expected_error_message = 'Missing request(s) for "given spam, ' \ - 'upon receiving eggs"' assert type(test_result) is Left - assert test_result.value == expected_error_message + assert test_result.value['status'] == 'FAILED' def test_non_matching_http_method(): From f26a56d9590aa5c31532347f11124266c602f607 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Fri, 17 Nov 2017 11:49:06 +1100 Subject: [PATCH 72/85] Fixes for logger --- pact_test/utils/logger.py | 18 ++++++++++++------ setup.py | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/pact_test/utils/logger.py b/pact_test/utils/logger.py index 6d3b950..8d299e9 100644 --- a/pact_test/utils/logger.py +++ b/pact_test/utils/logger.py @@ -26,12 +26,18 @@ def log_consumers_test_results(test_results): print() info('Test: ' + test_result.value['test']) for result in test_result.value['results']: - info(' GIVEN ' + result.value['state'] + ' UPON RECEIVING ' + result.value['description']) - info(' status: ' + result.value['status']) - for test_error in result.value['errors']: - error(' expected: ' + str(test_error['expected'])) - error(' actual: ' + str(test_error['actual'])) - error(' message: ' + str(test_error['message'])) + if type(result.value) is dict: + info(' GIVEN ' + result.value['state'] + ' UPON RECEIVING ' + result.value['description']) + info(' status: ' + result.value['status']) + for test_error in result.value['errors']: + if type(test_error) is dict: + error(' expected: ' + str(test_error['expected'])) + error(' actual: ' + str(test_error['actual'])) + error(' message: ' + str(test_error['message'])) + else: + error(' message: ' + str(test_error)) + else: + error(' ' + str(result.value)) info('') info('Goodbye!') print() diff --git a/setup.py b/setup.py index 9ac7361..97abde6 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.3.66', + version='0.3.67', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), From 7c51a18faeec195fcd50920e65df7b23d3708169 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Fri, 17 Nov 2017 14:00:50 +1100 Subject: [PATCH 73/85] Fixes for local Pact files --- pact_test/clients/http_client.py | 9 ++++---- pact_test/matchers/request_matcher.py | 2 +- pact_test/repositories/pact_broker.py | 21 +++---------------- pact_test/runners/pact_tests_runner.py | 12 ++++++----- .../runners/service_consumers/test_suite.py | 2 +- .../runners/service_providers/request_test.py | 7 +++++-- pact_test/servers/mock_server.py | 1 - pact_test/utils/logger.py | 6 +++--- pact_test/utils/pact_helper_utils.py | 16 ++++++++++++++ setup.py | 2 +- tests/clients/http_client.py | 1 - 11 files changed, 41 insertions(+), 38 deletions(-) diff --git a/pact_test/clients/http_client.py b/pact_test/clients/http_client.py index 6b6e630..173a6a7 100644 --- a/pact_test/clients/http_client.py +++ b/pact_test/clients/http_client.py @@ -1,6 +1,7 @@ import requests from pact_test.either import * from pact_test.constants import * +from pact_test.utils.logger import * from pact_test.models.response import PactResponse @@ -12,11 +13,12 @@ def execute_interaction_request(url, port, interaction): if type(server_response) is Right: headers = _parse_headers(server_response.value) content_type = _get_content_type(headers) - return Right(PactResponse( + out = Right(PactResponse( status=server_response.value.status_code, headers=headers, body=_parse_body(server_response.value, content_type) )) + return out return server_response @@ -29,10 +31,7 @@ def _server_response(method, url): def _parse_body(server_response, content_type): - if JSON in content_type: - return server_response.json() - else: - return server_response.text() + return server_response.json() if JSON in content_type else server_response.text def _parse_headers(server_response): diff --git a/pact_test/matchers/request_matcher.py b/pact_test/matchers/request_matcher.py index 26bb54d..35da3ff 100644 --- a/pact_test/matchers/request_matcher.py +++ b/pact_test/matchers/request_matcher.py @@ -1,5 +1,5 @@ -from pact_test.utils.logger import * from pact_test.either import * +from pact_test.utils.logger import * from pact_test.matchers.matcher import * diff --git a/pact_test/repositories/pact_broker.py b/pact_test/repositories/pact_broker.py index 5f60913..9126b2f 100644 --- a/pact_test/repositories/pact_broker.py +++ b/pact_test/repositories/pact_broker.py @@ -1,13 +1,14 @@ import json import requests from pact_test.either import * +from pact_test.utils.logger import * +from pact_test.utils.pact_helper_utils import format_headers PACT_BROKER_URL = 'http://localhost:9292/' def upload_pact(provider_name, consumer_name, pact, base_url=PACT_BROKER_URL): - print() pact = format_headers(pact) current_version = get_latest_version(consumer_name) if type(current_version) is Right: @@ -21,7 +22,7 @@ def upload_pact(provider_name, consumer_name, pact, base_url=PACT_BROKER_URL): except requests.exceptions.ConnectionError as e: msg = 'Failed to establish a new connection with ' + base_url return Left(msg) - print() + return current_version def get_latest_version(consumer_name, base_url=PACT_BROKER_URL): @@ -40,19 +41,3 @@ def next_version(current_version='1.0.0'): versions = current_version.split('.') next_minor = str(1 + int(versions[-1])) return '.'.join([versions[0], versions[1], next_minor]) - - -def format_headers(pact): - for interaction in pact.get('interactions', []): - req_headers = interaction.get('request').get('headers') - fixed_req_headers = {} - for h in req_headers: - fixed_req_headers.update(h) - interaction['request']['headers'] = fixed_req_headers - - res_headers = interaction.get('response').get('headers') - fixes_req_headers = {} - for h in res_headers: - fixes_req_headers.update(h) - interaction['response']['headers'] = fixes_req_headers - return pact diff --git a/pact_test/runners/pact_tests_runner.py b/pact_test/runners/pact_tests_runner.py index 12e229b..a575af9 100644 --- a/pact_test/runners/pact_tests_runner.py +++ b/pact_test/runners/pact_tests_runner.py @@ -1,9 +1,11 @@ import os import json +import copy from pact_test.either import * -from pact_test.utils.logger import info +from pact_test.utils.logger import * from pact_test.config.config_builder import Config from pact_test.repositories.pact_broker import upload_pact +from pact_test.utils.pact_helper_utils import format_headers from pact_test.utils.logger import log_consumers_test_results from pact_test.utils.logger import log_providers_test_results from pact_test.runners.service_consumers.test_suite import ServiceConsumerTestSuiteRunner @@ -28,8 +30,8 @@ def run_provider_tests(config): test_results = ServiceProviderTestSuiteRunner(config).verify() log_providers_test_results(test_results) if type(test_results) is Right: - write_pact_files(config, test_results.value) - upload_pacts(config, test_results.value) + write_pact_files(config, copy.copy(test_results.value)) + upload_pacts(config, copy.copy(test_results.value)) def upload_pacts(config, pacts): @@ -39,8 +41,8 @@ def upload_pacts(config, pacts): is_uploadable = all(interaction['status'] == 'PASSED' for interaction in pact['interactions']) if is_uploadable: ack = upload_pact(provider, consumer, pact, base_url=config.pact_broker_uri) - if type(ack) is Left: - error(ack.value) + msg = 'Pact between ' + pact['consumer']['name'] + ' and ' + pact['provider']['name'] + error(ack.value) if type(ack) is Left else info(msg + ' successfully uploaded') def write_pact_files(config, pacts): diff --git a/pact_test/runners/service_consumers/test_suite.py b/pact_test/runners/service_consumers/test_suite.py index aaa28e8..9abe39d 100644 --- a/pact_test/runners/service_consumers/test_suite.py +++ b/pact_test/runners/service_consumers/test_suite.py @@ -16,7 +16,7 @@ def __init__(self, config): self.config = config def verify(self): - print() + print('') debug('Verify consumers: START') pact_helper = load_pact_helper(self.config.consumer_tests_path) if type(pact_helper) is Right: diff --git a/pact_test/runners/service_providers/request_test.py b/pact_test/runners/service_providers/request_test.py index cd86bad..0595df6 100644 --- a/pact_test/runners/service_providers/request_test.py +++ b/pact_test/runners/service_providers/request_test.py @@ -1,5 +1,6 @@ -from pact_test.utils.logger import * +import json from pact_test.either import * +from pact_test.utils.logger import * from pact_test.models.request import PactRequest from pact_test.models.response import PactResponse from pact_test.servers.mock_server import MockServer @@ -31,12 +32,14 @@ def verify_request(decorated_method, port=9999): matching_result = match(actual_request, expected_request) if type(matching_result) is Right: + resp = mock_response.__dict__ + resp['body'] = json.loads(mock_response.__dict__['body']) out = { 'status': 'PASSED', 'providerState': decorated_method.given, 'description': decorated_method.upon_receiving, 'request': actual_request.__dict__, - 'response': mock_response.__dict__ + 'response': resp } return Right(out) else: diff --git a/pact_test/servers/mock_server.py b/pact_test/servers/mock_server.py index 8f5a17e..0d4f7b5 100644 --- a/pact_test/servers/mock_server.py +++ b/pact_test/servers/mock_server.py @@ -1,5 +1,4 @@ import json -from pact_test.utils.logger import debug from threading import Thread from pact_test.models.response import PactResponse try: diff --git a/pact_test/utils/logger.py b/pact_test/utils/logger.py index 8d299e9..9a06b01 100644 --- a/pact_test/utils/logger.py +++ b/pact_test/utils/logger.py @@ -23,7 +23,7 @@ def log_consumers_test_results(test_results): error(test_results.value.value) else: for test_result in test_results.value: - print() + print('') info('Test: ' + test_result.value['test']) for result in test_result.value['results']: if type(result.value) is dict: @@ -40,7 +40,7 @@ def log_consumers_test_results(test_results): error(' ' + str(result.value)) info('') info('Goodbye!') - print() + print('') def log_providers_test_results(test_results): @@ -48,7 +48,7 @@ def log_providers_test_results(test_results): error(test_results.value) else: for r in test_results.value: - print() + print('') info('A pact between ' + r['consumer']['name'] + ' and ' + r['provider']['name']) for i in r['interactions']: if i['status'] == 'FAILED': diff --git a/pact_test/utils/pact_helper_utils.py b/pact_test/utils/pact_helper_utils.py index ad23354..92919f4 100644 --- a/pact_test/utils/pact_helper_utils.py +++ b/pact_test/utils/pact_helper_utils.py @@ -45,3 +45,19 @@ def _path_to_pact_helper(consumer_tests_path): msg = MISSING_PACT_HELPER + consumer_tests_path + '".' return Left(msg) return Right(path) + + +def format_headers(pact): + for interaction in pact.get('interactions', []): + req_headers = interaction.get('request').get('headers') + fixed_req_headers = {} + for h in req_headers: + fixed_req_headers.update(h) + interaction['request']['headers'] = fixed_req_headers + + res_headers = interaction.get('response').get('headers') + fixes_req_headers = {} + for h in res_headers: + fixes_req_headers.update(h) + interaction['response']['headers'] = fixes_req_headers + return pact diff --git a/setup.py b/setup.py index 97abde6..875a0ca 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.3.67', + version='0.3.72', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), diff --git a/tests/clients/http_client.py b/tests/clients/http_client.py index 12b2d18..e599149 100644 --- a/tests/clients/http_client.py +++ b/tests/clients/http_client.py @@ -50,7 +50,6 @@ def text(self): assert response.status == 200 assert response.headers == [('Date', '12-06-2017')] - assert response.body == 'Spam & Eggs' def test_parse_headers(): From 9432780232fe990b85aa65111537e21410e87494 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Fri, 17 Nov 2017 15:28:50 +1100 Subject: [PATCH 74/85] Acceptance tests for method --- docker-compose.yml | 10 +++++----- pact_test/matchers/request_matcher.py | 4 ++-- ...different method.json => different_method.json} | 0 .../testcases/request/method/different_method.py | 14 ++++++++++++++ .../testcases/request/method/matches.json | 3 +-- .../version_1/testcases/request/method/matches.py | 14 ++++++++++++++ ...ent case.json => method_is_different_case.json} | 0 .../request/method/method_is_different_case.py | 14 ++++++++++++++ 8 files changed, 50 insertions(+), 9 deletions(-) rename tests/acceptance/version_1/testcases/request/method/{different method.json => different_method.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/method/different_method.py create mode 100644 tests/acceptance/version_1/testcases/request/method/matches.py rename tests/acceptance/version_1/testcases/request/method/{method is different case.json => method_is_different_case.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/method/method_is_different_case.py diff --git a/docker-compose.yml b/docker-compose.yml index 662871a..07e0f3b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ version: '2.1' services: py27: - command: sh -c 'find . -name '*.pyc' -delete && python setup.py test' + command: sh -c "find . -name '*.pyc' -delete && python setup.py test" volumes: - .:/app build: @@ -11,7 +11,7 @@ services: dockerfile: '/environments/Dockerfilepy27' py33: - command: sh -c 'find . -name '*.pyc' -delete && python setup.py test' + command: sh -c "find . -name '*.pyc' -delete && python setup.py test" volumes: - .:/app build: @@ -19,7 +19,7 @@ services: dockerfile: '/environments/Dockerfilepy33' py34: - command: sh -c 'find . -name '*.pyc' -delete && python setup.py test' + command: sh -c "find . -name '*.pyc' -delete && python setup.py test" volumes: - .:/app build: @@ -27,7 +27,7 @@ services: dockerfile: '/environments/Dockerfilepy34' py35: - command: sh -c 'find . -name '*.pyc' -delete && python setup.py test' + command: sh -c "find . -name '*.pyc' -delete && python setup.py test" volumes: - .:/app build: @@ -35,7 +35,7 @@ services: dockerfile: '/environments/Dockerfilepy35' py36: - command: sh -c 'find . -name '*.pyc' -delete && python setup.py test' + command: sh -c "find . -name '*.pyc' -delete && python setup.py test" volumes: - .:/app build: diff --git a/pact_test/matchers/request_matcher.py b/pact_test/matchers/request_matcher.py index 35da3ff..007d317 100644 --- a/pact_test/matchers/request_matcher.py +++ b/pact_test/matchers/request_matcher.py @@ -33,8 +33,6 @@ def _match_dicts_all_keys_and_values(d1, d2): d1_keys = d1.keys() d2_keys = d2.keys() - # delete_extra_keys(d1, d2) - all_keys = set(d2_keys).issubset(set(d1_keys)) all_values = match_dicts_all_values(d1, d2) @@ -54,6 +52,8 @@ def _match_headers(actual, expected): def _match_path(actual, expected): + error(actual) + error(expected) if actual.path == expected.path: return Right(actual) return Left(build_error_message('path', expected.path, actual.path)) diff --git a/tests/acceptance/version_1/testcases/request/method/different method.json b/tests/acceptance/version_1/testcases/request/method/different_method.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/method/different method.json rename to tests/acceptance/version_1/testcases/request/method/different_method.json diff --git a/tests/acceptance/version_1/testcases/request/method/different_method.py b/tests/acceptance/version_1/testcases/request/method/different_method.py new file mode 100644 index 0000000..9deb57a --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/method/different_method.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_method(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/method/matches.json b/tests/acceptance/version_1/testcases/request/method/matches.json index be94961..58c993f 100755 --- a/tests/acceptance/version_1/testcases/request/method/matches.json +++ b/tests/acceptance/version_1/testcases/request/method/matches.json @@ -12,6 +12,5 @@ "path": "/", "query": "", "headers": {} - } -} \ No newline at end of file +} diff --git a/tests/acceptance/version_1/testcases/request/method/matches.py b/tests/acceptance/version_1/testcases/request/method/matches.py new file mode 100644 index 0000000..a2ef6ce --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/method/matches.py @@ -0,0 +1,14 @@ +from pact_test.either import Right +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_status_matches(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Right diff --git a/tests/acceptance/version_1/testcases/request/method/method is different case.json b/tests/acceptance/version_1/testcases/request/method/method_is_different_case.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/method/method is different case.json rename to tests/acceptance/version_1/testcases/request/method/method_is_different_case.json diff --git a/tests/acceptance/version_1/testcases/request/method/method_is_different_case.py b/tests/acceptance/version_1/testcases/request/method/method_is_different_case.py new file mode 100644 index 0000000..4171c33 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/method/method_is_different_case.py @@ -0,0 +1,14 @@ +from pact_test.either import Right +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test_different_method_case(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Right From f3b6899b9e519ac20b81f0e56564ee7d4728c0e2 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Fri, 17 Nov 2017 15:38:24 +1100 Subject: [PATCH 75/85] Acceptance tests for path --- .../version_1/testcases/request/method/__init__.py | 0 .../testcases/request/method/different_method.py | 2 +- .../version_1/testcases/request/method/matches.py | 2 +- .../request/method/method_is_different_case.py | 2 +- .../version_1/testcases/request/path/__init__.py | 0 ...ty_path_found_when_forward_slash_expected.json} | 0 ...empty_path_found_when_forward_slash_expected.py | 14 ++++++++++++++ ...ward_slash_found_when_empty_path_expected.json} | 0 ...forward_slash_found_when_empty_path_expected.py | 14 ++++++++++++++ .../{incorrect path.json => incorrect_path.json} | 0 .../testcases/request/path/incorrect_path.py | 14 ++++++++++++++ .../version_1/testcases/request/path/matches.py | 14 ++++++++++++++ ...th.json => missing_trailing_slash_in_path.json} | 0 .../request/path/missing_trailing_slash_in_path.py | 14 ++++++++++++++ ...json => unexpected_trailing_slash_in_path.json} | 0 .../path/unexpected_trailing_slash_in_path.py | 14 ++++++++++++++ 16 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 tests/acceptance/version_1/testcases/request/method/__init__.py create mode 100644 tests/acceptance/version_1/testcases/request/path/__init__.py rename tests/acceptance/version_1/testcases/request/path/{empty path found when forward slash expected.json => empty_path_found_when_forward_slash_expected.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/path/empty_path_found_when_forward_slash_expected.py rename tests/acceptance/version_1/testcases/request/path/{forward slash found when empty path expected.json => forward_slash_found_when_empty_path_expected.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/path/forward_slash_found_when_empty_path_expected.py rename tests/acceptance/version_1/testcases/request/path/{incorrect path.json => incorrect_path.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/path/incorrect_path.py create mode 100644 tests/acceptance/version_1/testcases/request/path/matches.py rename tests/acceptance/version_1/testcases/request/path/{missing trailing slash in path.json => missing_trailing_slash_in_path.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/path/missing_trailing_slash_in_path.py rename tests/acceptance/version_1/testcases/request/path/{unexpected trailing slash in path.json => unexpected_trailing_slash_in_path.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/path/unexpected_trailing_slash_in_path.py diff --git a/tests/acceptance/version_1/testcases/request/method/__init__.py b/tests/acceptance/version_1/testcases/request/method/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/acceptance/version_1/testcases/request/method/different_method.py b/tests/acceptance/version_1/testcases/request/method/different_method.py index 9deb57a..b37da56 100644 --- a/tests/acceptance/version_1/testcases/request/method/different_method.py +++ b/tests/acceptance/version_1/testcases/request/method/different_method.py @@ -4,7 +4,7 @@ from tests.acceptance.acceptance_test_loader import load_acceptance_test -def test_different_method(): +def test(): data = load_acceptance_test(__file__) actual = PactRequest(**data['actual']) diff --git a/tests/acceptance/version_1/testcases/request/method/matches.py b/tests/acceptance/version_1/testcases/request/method/matches.py index a2ef6ce..2ef0bea 100644 --- a/tests/acceptance/version_1/testcases/request/method/matches.py +++ b/tests/acceptance/version_1/testcases/request/method/matches.py @@ -4,7 +4,7 @@ from tests.acceptance.acceptance_test_loader import load_acceptance_test -def test_status_matches(): +def test(): data = load_acceptance_test(__file__) actual = PactRequest(**data['actual']) diff --git a/tests/acceptance/version_1/testcases/request/method/method_is_different_case.py b/tests/acceptance/version_1/testcases/request/method/method_is_different_case.py index 4171c33..2ef0bea 100644 --- a/tests/acceptance/version_1/testcases/request/method/method_is_different_case.py +++ b/tests/acceptance/version_1/testcases/request/method/method_is_different_case.py @@ -4,7 +4,7 @@ from tests.acceptance.acceptance_test_loader import load_acceptance_test -def test_different_method_case(): +def test(): data = load_acceptance_test(__file__) actual = PactRequest(**data['actual']) diff --git a/tests/acceptance/version_1/testcases/request/path/__init__.py b/tests/acceptance/version_1/testcases/request/path/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/acceptance/version_1/testcases/request/path/empty path found when forward slash expected.json b/tests/acceptance/version_1/testcases/request/path/empty_path_found_when_forward_slash_expected.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/path/empty path found when forward slash expected.json rename to tests/acceptance/version_1/testcases/request/path/empty_path_found_when_forward_slash_expected.json diff --git a/tests/acceptance/version_1/testcases/request/path/empty_path_found_when_forward_slash_expected.py b/tests/acceptance/version_1/testcases/request/path/empty_path_found_when_forward_slash_expected.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/path/empty_path_found_when_forward_slash_expected.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/path/forward slash found when empty path expected.json b/tests/acceptance/version_1/testcases/request/path/forward_slash_found_when_empty_path_expected.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/path/forward slash found when empty path expected.json rename to tests/acceptance/version_1/testcases/request/path/forward_slash_found_when_empty_path_expected.json diff --git a/tests/acceptance/version_1/testcases/request/path/forward_slash_found_when_empty_path_expected.py b/tests/acceptance/version_1/testcases/request/path/forward_slash_found_when_empty_path_expected.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/path/forward_slash_found_when_empty_path_expected.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/path/incorrect path.json b/tests/acceptance/version_1/testcases/request/path/incorrect_path.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/path/incorrect path.json rename to tests/acceptance/version_1/testcases/request/path/incorrect_path.json diff --git a/tests/acceptance/version_1/testcases/request/path/incorrect_path.py b/tests/acceptance/version_1/testcases/request/path/incorrect_path.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/path/incorrect_path.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/path/matches.py b/tests/acceptance/version_1/testcases/request/path/matches.py new file mode 100644 index 0000000..2ef0bea --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/path/matches.py @@ -0,0 +1,14 @@ +from pact_test.either import Right +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Right diff --git a/tests/acceptance/version_1/testcases/request/path/missing trailing slash in path.json b/tests/acceptance/version_1/testcases/request/path/missing_trailing_slash_in_path.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/path/missing trailing slash in path.json rename to tests/acceptance/version_1/testcases/request/path/missing_trailing_slash_in_path.json diff --git a/tests/acceptance/version_1/testcases/request/path/missing_trailing_slash_in_path.py b/tests/acceptance/version_1/testcases/request/path/missing_trailing_slash_in_path.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/path/missing_trailing_slash_in_path.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/path/unexpected trailing slash in path.json b/tests/acceptance/version_1/testcases/request/path/unexpected_trailing_slash_in_path.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/path/unexpected trailing slash in path.json rename to tests/acceptance/version_1/testcases/request/path/unexpected_trailing_slash_in_path.json diff --git a/tests/acceptance/version_1/testcases/request/path/unexpected_trailing_slash_in_path.py b/tests/acceptance/version_1/testcases/request/path/unexpected_trailing_slash_in_path.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/path/unexpected_trailing_slash_in_path.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left From 9d4af2b169edef1116cebcd02621108e0f98ae31 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Fri, 17 Nov 2017 16:11:49 +1100 Subject: [PATCH 76/85] Acceptance tests for query --- pact_test/matchers/request_matcher.py | 18 +++++++++++++++--- .../testcases/request/query/__init__.py | 0 ...m order.json => different_param_order.json} | 0 .../request/query/different_param_order.py | 14 ++++++++++++++ ...values.json => different_param_values.json} | 0 .../request/query/different_param_values.py | 14 ++++++++++++++ .../testcases/request/query/matches.py | 14 ++++++++++++++ ...atches_with_equals_in_the_query_value.json} | 0 .../matches_with_equals_in_the_query_value.py | 14 ++++++++++++++ ...g amperand.json => trailing_ampersand.json} | 0 .../request/query/trailing_ampersand.py | 14 ++++++++++++++ 11 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 tests/acceptance/version_1/testcases/request/query/__init__.py rename tests/acceptance/version_1/testcases/request/query/{different param order.json => different_param_order.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/query/different_param_order.py rename tests/acceptance/version_1/testcases/request/query/{different param values.json => different_param_values.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/query/different_param_values.py create mode 100644 tests/acceptance/version_1/testcases/request/query/matches.py rename tests/acceptance/version_1/testcases/request/query/{matches with equals in the query value.json => matches_with_equals_in_the_query_value.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/query/matches_with_equals_in_the_query_value.py rename tests/acceptance/version_1/testcases/request/query/{trailing amperand.json => trailing_ampersand.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/query/trailing_ampersand.py diff --git a/pact_test/matchers/request_matcher.py b/pact_test/matchers/request_matcher.py index 007d317..cf91254 100644 --- a/pact_test/matchers/request_matcher.py +++ b/pact_test/matchers/request_matcher.py @@ -1,6 +1,10 @@ from pact_test.either import * from pact_test.utils.logger import * from pact_test.matchers.matcher import * +try: + import urllib.parse as parse +except ImportError: + import urllib as parse def match(actual, expected): @@ -52,19 +56,27 @@ def _match_headers(actual, expected): def _match_path(actual, expected): - error(actual) - error(expected) if actual.path == expected.path: return Right(actual) return Left(build_error_message('path', expected.path, actual.path)) def _match_query(actual, expected): - if actual.query == expected.query: + debug('') + debug('') + debug(_encode(actual.query)) + debug(_encode(expected.query)) + debug('') + debug('') + if _encode(actual.query) == _encode(expected.query): return Right(actual) return Left(build_error_message('query', expected.query, actual.query)) +def _encode(str): + return parse.quote(str, safe="%3D") + + def _match_method(actual, expected): if actual.method.upper() == expected.method.upper(): return Right(actual) diff --git a/tests/acceptance/version_1/testcases/request/query/__init__.py b/tests/acceptance/version_1/testcases/request/query/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/acceptance/version_1/testcases/request/query/different param order.json b/tests/acceptance/version_1/testcases/request/query/different_param_order.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/query/different param order.json rename to tests/acceptance/version_1/testcases/request/query/different_param_order.json diff --git a/tests/acceptance/version_1/testcases/request/query/different_param_order.py b/tests/acceptance/version_1/testcases/request/query/different_param_order.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/query/different_param_order.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/query/different param values.json b/tests/acceptance/version_1/testcases/request/query/different_param_values.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/query/different param values.json rename to tests/acceptance/version_1/testcases/request/query/different_param_values.json diff --git a/tests/acceptance/version_1/testcases/request/query/different_param_values.py b/tests/acceptance/version_1/testcases/request/query/different_param_values.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/query/different_param_values.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/query/matches.py b/tests/acceptance/version_1/testcases/request/query/matches.py new file mode 100644 index 0000000..2ef0bea --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/query/matches.py @@ -0,0 +1,14 @@ +from pact_test.either import Right +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Right diff --git a/tests/acceptance/version_1/testcases/request/query/matches with equals in the query value.json b/tests/acceptance/version_1/testcases/request/query/matches_with_equals_in_the_query_value.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/query/matches with equals in the query value.json rename to tests/acceptance/version_1/testcases/request/query/matches_with_equals_in_the_query_value.json diff --git a/tests/acceptance/version_1/testcases/request/query/matches_with_equals_in_the_query_value.py b/tests/acceptance/version_1/testcases/request/query/matches_with_equals_in_the_query_value.py new file mode 100644 index 0000000..2ef0bea --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/query/matches_with_equals_in_the_query_value.py @@ -0,0 +1,14 @@ +from pact_test.either import Right +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Right diff --git a/tests/acceptance/version_1/testcases/request/query/trailing amperand.json b/tests/acceptance/version_1/testcases/request/query/trailing_ampersand.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/query/trailing amperand.json rename to tests/acceptance/version_1/testcases/request/query/trailing_ampersand.json diff --git a/tests/acceptance/version_1/testcases/request/query/trailing_ampersand.py b/tests/acceptance/version_1/testcases/request/query/trailing_ampersand.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/query/trailing_ampersand.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left From ccd6ecefa2ca3e891320880e31c62cd80b65c793 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Mon, 20 Nov 2017 17:43:25 +1100 Subject: [PATCH 77/85] Acceptance tests for headers --- pact_test/matchers/request_matcher.py | 14 ++++---------- pact_test/repositories/pact_broker.py | 2 +- pact_test/runners/pact_tests_runner.py | 9 +++++---- setup.py | 2 +- .../testcases/request/headers/__init__.py | 0 .../{empty headers.json => empty_headers.json} | 0 .../testcases/request/headers/empty_headers.py | 14 ++++++++++++++ ...json => header_name_is_different_case.json} | 0 .../headers/header_name_is_different_case.py | 18 ++++++++++++++++++ ...son => header_value_is_different_case.json} | 0 .../headers/header_value_is_different_case.py | 18 ++++++++++++++++++ .../testcases/request/headers/matches.py | 18 ++++++++++++++++++ ...mma_separated_header_values_different.json} | 0 ..._comma_separated_header_values_different.py | 18 ++++++++++++++++++ ...found.json => unexpected_header_found.json} | 0 .../request/headers/unexpected_header_found.py | 18 ++++++++++++++++++ ...n => whitespace_after_comma_different.json} | 0 .../whitespace_after_comma_different.py | 18 ++++++++++++++++++ 18 files changed, 133 insertions(+), 16 deletions(-) create mode 100644 tests/acceptance/version_1/testcases/request/headers/__init__.py rename tests/acceptance/version_1/testcases/request/headers/{empty headers.json => empty_headers.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/headers/empty_headers.py rename tests/acceptance/version_1/testcases/request/headers/{header name is different case.json => header_name_is_different_case.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/headers/header_name_is_different_case.py rename tests/acceptance/version_1/testcases/request/headers/{header value is different case.json => header_value_is_different_case.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/headers/header_value_is_different_case.py create mode 100644 tests/acceptance/version_1/testcases/request/headers/matches.py rename tests/acceptance/version_1/testcases/request/headers/{order of comma separated header values different.json => order_of_comma_separated_header_values_different.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/headers/order_of_comma_separated_header_values_different.py rename tests/acceptance/version_1/testcases/request/headers/{unexpected header found.json => unexpected_header_found.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/headers/unexpected_header_found.py rename tests/acceptance/version_1/testcases/request/headers/{whitespace after comma different.json => whitespace_after_comma_different.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/headers/whitespace_after_comma_different.py diff --git a/pact_test/matchers/request_matcher.py b/pact_test/matchers/request_matcher.py index cf91254..3b9ef0e 100644 --- a/pact_test/matchers/request_matcher.py +++ b/pact_test/matchers/request_matcher.py @@ -44,11 +44,11 @@ def _match_dicts_all_keys_and_values(d1, d2): def _match_headers(actual, expected): - actual_dict = dict(pair for d in actual.headers for pair in d.items()) - expected_dict = dict(pair for d in expected.headers for pair in d.items()) + actual_dict = dict(pair for d in actual.headers for pair in sorted(d.items())) + expected_dict = dict(pair for d in expected.headers for pair in sorted(d.items())) - insensitive_actual = {k.upper(): v for (k, v) in actual_dict.items()} - insensitive_expected = {k.upper(): v for (k, v) in expected_dict.items()} + insensitive_actual = {k.upper(): v for (k, v) in sorted(actual_dict.items())} + insensitive_expected = {k.upper(): v for (k, v) in sorted(expected_dict.items())} if is_subset(insensitive_expected, insensitive_actual): return Right(actual) @@ -62,12 +62,6 @@ def _match_path(actual, expected): def _match_query(actual, expected): - debug('') - debug('') - debug(_encode(actual.query)) - debug(_encode(expected.query)) - debug('') - debug('') if _encode(actual.query) == _encode(expected.query): return Right(actual) return Left(build_error_message('query', expected.query, actual.query)) diff --git a/pact_test/repositories/pact_broker.py b/pact_test/repositories/pact_broker.py index 9126b2f..ad899dd 100644 --- a/pact_test/repositories/pact_broker.py +++ b/pact_test/repositories/pact_broker.py @@ -10,7 +10,7 @@ def upload_pact(provider_name, consumer_name, pact, base_url=PACT_BROKER_URL): pact = format_headers(pact) - current_version = get_latest_version(consumer_name) + current_version = get_latest_version(consumer_name, base_url) if type(current_version) is Right: v = next_version(current_version.value) try: diff --git a/pact_test/runners/pact_tests_runner.py b/pact_test/runners/pact_tests_runner.py index a575af9..faa7e7b 100644 --- a/pact_test/runners/pact_tests_runner.py +++ b/pact_test/runners/pact_tests_runner.py @@ -30,8 +30,8 @@ def run_provider_tests(config): test_results = ServiceProviderTestSuiteRunner(config).verify() log_providers_test_results(test_results) if type(test_results) is Right: - write_pact_files(config, copy.copy(test_results.value)) - upload_pacts(config, copy.copy(test_results.value)) + write_pact_files(config, copy.deepcopy(test_results.value)) + upload_pacts(config, copy.deepcopy(test_results.value)) def upload_pacts(config, pacts): @@ -52,8 +52,9 @@ def write_pact_files(config, pacts): os.makedirs(pacts_directory) for pact in pacts: - filename = pact['consumer']['name'] + '_' + pact['provider']['name'] + '.json' + pact_payload = format_headers(copy.deepcopy(pact)) + filename = pact_payload['consumer']['name'] + '_' + pact_payload['provider']['name'] + '.json' filename = filename.replace(' ', '_').lower() info('Writing pact to: ' + filename) with open(os.path.join(pacts_directory, filename), 'w+') as file: - file.write(json.dumps(pact, indent=2)) + file.write(json.dumps(pact_payload, indent=2)) diff --git a/setup.py b/setup.py index 875a0ca..10ca16e 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.3.72', + version='0.3.79', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), diff --git a/tests/acceptance/version_1/testcases/request/headers/__init__.py b/tests/acceptance/version_1/testcases/request/headers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/acceptance/version_1/testcases/request/headers/empty headers.json b/tests/acceptance/version_1/testcases/request/headers/empty_headers.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/headers/empty headers.json rename to tests/acceptance/version_1/testcases/request/headers/empty_headers.json diff --git a/tests/acceptance/version_1/testcases/request/headers/empty_headers.py b/tests/acceptance/version_1/testcases/request/headers/empty_headers.py new file mode 100644 index 0000000..f0fc310 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/headers/empty_headers.py @@ -0,0 +1,14 @@ +from pact_test.either import Right +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(headers=[]) + expected = PactRequest(headers=[]) + + test_result = match(actual, expected) + assert type(test_result) is Right diff --git a/tests/acceptance/version_1/testcases/request/headers/header name is different case.json b/tests/acceptance/version_1/testcases/request/headers/header_name_is_different_case.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/headers/header name is different case.json rename to tests/acceptance/version_1/testcases/request/headers/header_name_is_different_case.json diff --git a/tests/acceptance/version_1/testcases/request/headers/header_name_is_different_case.py b/tests/acceptance/version_1/testcases/request/headers/header_name_is_different_case.py new file mode 100644 index 0000000..503be41 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/headers/header_name_is_different_case.py @@ -0,0 +1,18 @@ +from pact_test.either import Right +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest( + headers=[{'ACCEPT': 'alligators'}] + ) + expected = PactRequest( + headers=[{'Accept': 'alligators'}] + ) + + test_result = match(actual, expected) + assert type(test_result) is Right diff --git a/tests/acceptance/version_1/testcases/request/headers/header value is different case.json b/tests/acceptance/version_1/testcases/request/headers/header_value_is_different_case.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/headers/header value is different case.json rename to tests/acceptance/version_1/testcases/request/headers/header_value_is_different_case.json diff --git a/tests/acceptance/version_1/testcases/request/headers/header_value_is_different_case.py b/tests/acceptance/version_1/testcases/request/headers/header_value_is_different_case.py new file mode 100644 index 0000000..e05f505 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/headers/header_value_is_different_case.py @@ -0,0 +1,18 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest( + headers=[{'Accept': 'Alligators'}] + ) + expected = PactRequest( + headers=[{'Accept': 'alligators'}] + ) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/headers/matches.py b/tests/acceptance/version_1/testcases/request/headers/matches.py new file mode 100644 index 0000000..9c0315e --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/headers/matches.py @@ -0,0 +1,18 @@ +from pact_test.either import Right +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest( + headers=[{'Content-Type': 'hippos'}, {'Accept': 'alligators'}] + ) + expected = PactRequest( + headers=[{'Accept': 'alligators'}, {'Content-Type': 'hippos'}] + ) + + test_result = match(actual, expected) + assert type(test_result) is Right diff --git a/tests/acceptance/version_1/testcases/request/headers/order of comma separated header values different.json b/tests/acceptance/version_1/testcases/request/headers/order_of_comma_separated_header_values_different.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/headers/order of comma separated header values different.json rename to tests/acceptance/version_1/testcases/request/headers/order_of_comma_separated_header_values_different.json diff --git a/tests/acceptance/version_1/testcases/request/headers/order_of_comma_separated_header_values_different.py b/tests/acceptance/version_1/testcases/request/headers/order_of_comma_separated_header_values_different.py new file mode 100644 index 0000000..716b33b --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/headers/order_of_comma_separated_header_values_different.py @@ -0,0 +1,18 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest( + headers=[{'Accept': 'hippos, alligators'}] + ) + expected = PactRequest( + headers=[{'Accept': 'alligators, hippos'}] + ) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/headers/unexpected header found.json b/tests/acceptance/version_1/testcases/request/headers/unexpected_header_found.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/headers/unexpected header found.json rename to tests/acceptance/version_1/testcases/request/headers/unexpected_header_found.json diff --git a/tests/acceptance/version_1/testcases/request/headers/unexpected_header_found.py b/tests/acceptance/version_1/testcases/request/headers/unexpected_header_found.py new file mode 100644 index 0000000..ae80b72 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/headers/unexpected_header_found.py @@ -0,0 +1,18 @@ +from pact_test.either import Right +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest( + headers=[{'Accept': 'alligators'}] + ) + expected = PactRequest( + headers=[] + ) + + test_result = match(actual, expected) + assert type(test_result) is Right diff --git a/tests/acceptance/version_1/testcases/request/headers/whitespace after comma different.json b/tests/acceptance/version_1/testcases/request/headers/whitespace_after_comma_different.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/headers/whitespace after comma different.json rename to tests/acceptance/version_1/testcases/request/headers/whitespace_after_comma_different.json diff --git a/tests/acceptance/version_1/testcases/request/headers/whitespace_after_comma_different.py b/tests/acceptance/version_1/testcases/request/headers/whitespace_after_comma_different.py new file mode 100644 index 0000000..69162e1 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/headers/whitespace_after_comma_different.py @@ -0,0 +1,18 @@ +from pact_test.either import Right +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest( + headers=[{'Accept': 'alligators, hippos'}] + ) + expected = PactRequest( + headers=[{'Accept': 'alligators,hippos'}] + ) + + test_result = match(actual, expected) + assert type(test_result) is Right From ed3f85c762f86a9b6dadbdceecced16b50dd713d Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 25 Nov 2017 10:38:28 +1100 Subject: [PATCH 78/85] Acceptance test for body --- README.rst | 25 +++++++++++++++++-- setup.cfg | 2 +- setup.py | 2 +- .../testcases/request/body/__init__.py | 0 ...der.json => array_in_different_order.json} | 0 .../request/body/array_in_different_order.py | 14 +++++++++++ ...on => different_value_found_at_index.json} | 0 .../body/different_value_found_at_index.py | 14 +++++++++++ ...json => different_value_found_at_key.json} | 0 .../body/different_value_found_at_key.py | 14 +++++++++++ .../testcases/request/body/matches.py | 14 +++++++++++ ...{missing index.json => missing_index.json} | 0 .../testcases/request/body/missing_index.py | 14 +++++++++++ .../{missing key.json => missing_key.json} | 0 .../testcases/request/body/missing_key.py | 14 +++++++++++ ...null_found_at_key_when_null_expected.json} | 0 ...ot_null_found_at_key_when_null_expected.py | 14 +++++++++++ ...ll_found_in_array_when_null_expected.json} | 0 ..._null_found_in_array_when_null_expected.py | 14 +++++++++++ ...found_at_key_where_not_null_expected.json} | 0 ...ll_found_at_key_where_not_null_expected.py | 14 +++++++++++ ...ound_in_array_when_not_null_expected.json} | 0 ...l_found_in_array_when_not_null_expected.py | 14 +++++++++++ ...er_found_at_key_when_string_expected.json} | 0 ...umber_found_at_key_when_string_expected.py | 14 +++++++++++ ..._found_in_array_when_string_expected.json} | 0 ...ber_found_in_array_when_string_expected.py | 14 +++++++++++ ...on => plain_text_that_does_not_match.json} | 0 .../body/plain_text_that_does_not_match.py | 17 +++++++++++++ ...ches.json => plain_text_that_matches.json} | 0 .../request/body/plain_text_that_matches.py | 17 +++++++++++++ ...ng_found_at_key_when_number_expected.json} | 0 ...tring_found_at_key_when_number_expected.py | 14 +++++++++++ ..._found_in_array_when_number_expected.json} | 0 ...ing_found_in_array_when_number_expected.py | 14 +++++++++++ ...unexpected_index_with_not_null_value.json} | 0 .../unexpected_index_with_not_null_value.py | 14 +++++++++++ ... => unexpected_index_with_null_value.json} | 0 .../body/unexpected_index_with_null_value.py | 14 +++++++++++ ...> unexpected_key_with_not_null_value.json} | 0 .../unexpected_key_with_not_null_value.py | 14 +++++++++++ ...on => unexpected_key_with_null_value.json} | 0 .../body/unexpected_key_with_null_value.py | 14 +++++++++++ 43 files changed, 311 insertions(+), 4 deletions(-) create mode 100644 tests/acceptance/version_1/testcases/request/body/__init__.py rename tests/acceptance/version_1/testcases/request/body/{array in different order.json => array_in_different_order.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/body/array_in_different_order.py rename tests/acceptance/version_1/testcases/request/body/{different value found at index.json => different_value_found_at_index.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/body/different_value_found_at_index.py rename tests/acceptance/version_1/testcases/request/body/{different value found at key.json => different_value_found_at_key.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/body/different_value_found_at_key.py create mode 100644 tests/acceptance/version_1/testcases/request/body/matches.py rename tests/acceptance/version_1/testcases/request/body/{missing index.json => missing_index.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/body/missing_index.py rename tests/acceptance/version_1/testcases/request/body/{missing key.json => missing_key.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/body/missing_key.py rename tests/acceptance/version_1/testcases/request/body/{not null found at key when null expected.json => not_null_found_at_key_when_null_expected.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/body/not_null_found_at_key_when_null_expected.py rename tests/acceptance/version_1/testcases/request/body/{not null found in array when null expected.json => not_null_found_in_array_when_null_expected.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/body/not_null_found_in_array_when_null_expected.py rename tests/acceptance/version_1/testcases/request/body/{null found at key where not null expected.json => null_found_at_key_where_not_null_expected.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/body/null_found_at_key_where_not_null_expected.py rename tests/acceptance/version_1/testcases/request/body/{null found in array when not null expected.json => null_found_in_array_when_not_null_expected.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/body/null_found_in_array_when_not_null_expected.py rename tests/acceptance/version_1/testcases/request/body/{number found at key when string expected.json => number_found_at_key_when_string_expected.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/body/number_found_at_key_when_string_expected.py rename tests/acceptance/version_1/testcases/request/body/{number found in array when string expected.json => number_found_in_array_when_string_expected.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/body/number_found_in_array_when_string_expected.py rename tests/acceptance/version_1/testcases/request/body/{plain text that does not match.json => plain_text_that_does_not_match.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/body/plain_text_that_does_not_match.py rename tests/acceptance/version_1/testcases/request/body/{plain text that matches.json => plain_text_that_matches.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/body/plain_text_that_matches.py rename tests/acceptance/version_1/testcases/request/body/{string found at key when number expected.json => string_found_at_key_when_number_expected.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/body/string_found_at_key_when_number_expected.py rename tests/acceptance/version_1/testcases/request/body/{string found in array when number expected.json => string_found_in_array_when_number_expected.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/body/string_found_in_array_when_number_expected.py rename tests/acceptance/version_1/testcases/request/body/{unexpected index with not null value.json => unexpected_index_with_not_null_value.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/body/unexpected_index_with_not_null_value.py rename tests/acceptance/version_1/testcases/request/body/{unexpected index with null value.json => unexpected_index_with_null_value.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/body/unexpected_index_with_null_value.py rename tests/acceptance/version_1/testcases/request/body/{unexpected key with not null value.json => unexpected_key_with_not_null_value.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/body/unexpected_key_with_not_null_value.py rename tests/acceptance/version_1/testcases/request/body/{unexpected key with null value.json => unexpected_key_with_null_value.json} (100%) create mode 100644 tests/acceptance/version_1/testcases/request/body/unexpected_key_with_null_value.py diff --git a/README.rst b/README.rst index d2b1392..15cdc25 100644 --- a/README.rst +++ b/README.rst @@ -26,7 +26,7 @@ after all!*), and a Provider honours it. Providers Tests (*Set the Contracts*) ------------------------------------- -.. image:: https://img.shields.io/badge/Pact-1.0-red.svg +.. image:: https://img.shields.io/badge/Pact-1.0-brightgreen.svg :target: https://github.com/pact-foundation/pact-specification/tree/version-1 .. image:: https://img.shields.io/badge/Pact-1.1-red.svg :target: https://github.com/pact-foundation/pact-specification/tree/version-1.1 @@ -37,7 +37,28 @@ Providers Tests (*Set the Contracts*) .. image:: https://img.shields.io/badge/Pact-4.0-red.svg :target: https://github.com/pact-foundation/pact-specification/tree/version-4 -TBD. +Consumers run Provider Tests to create pacts and establish a contract between +them and service providers. An example of a Python client using pact test is +available at `here `_. Consumers define +all the interactions with their providers in the following way: + +.. code:: python + + @service_consumer('UberEats') + @has_pact_with('Dominos Pizza') + class DominosPizzaTest(ServiceProviderTest): + + @given('some pizza exist') + @upon_receiving('a request for an hawaiian pizza') + @with_request({'method': 'get', 'path': '/pizzas/hawaiian/'}) + @will_respond_with({'status': 404, 'body': json.dumps({'reason': 'we do not serve pineapple with pizza'})}) + def test_get_pizza(self): + pizza = get_pizza('hawaiian') + assert pizza.status_code == 404 + +This test verifies, against a mock server, the expected interaction and creates +a JSON file (*the pact*) that will be stored locally and also sent to the +Pact Broker, if available. Consumers Tests (*Honour Your Contracts*) ----------------------------------------- diff --git a/setup.cfg b/setup.cfg index 1a295af..81428bc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ universal=1 test=pytest [tool:pytest] -addopts=--pep8 +addopts=--pep8 --maxfail=1 -rf testpaths=tests python_files=*.py norecursedirs=tests/resources diff --git a/setup.py b/setup.py index 10ca16e..57fd9a4 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.3.79', + version='0.3.80', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), diff --git a/tests/acceptance/version_1/testcases/request/body/__init__.py b/tests/acceptance/version_1/testcases/request/body/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/acceptance/version_1/testcases/request/body/array in different order.json b/tests/acceptance/version_1/testcases/request/body/array_in_different_order.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/body/array in different order.json rename to tests/acceptance/version_1/testcases/request/body/array_in_different_order.json diff --git a/tests/acceptance/version_1/testcases/request/body/array_in_different_order.py b/tests/acceptance/version_1/testcases/request/body/array_in_different_order.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/array_in_different_order.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/body/different value found at index.json b/tests/acceptance/version_1/testcases/request/body/different_value_found_at_index.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/body/different value found at index.json rename to tests/acceptance/version_1/testcases/request/body/different_value_found_at_index.json diff --git a/tests/acceptance/version_1/testcases/request/body/different_value_found_at_index.py b/tests/acceptance/version_1/testcases/request/body/different_value_found_at_index.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/different_value_found_at_index.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/body/different value found at key.json b/tests/acceptance/version_1/testcases/request/body/different_value_found_at_key.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/body/different value found at key.json rename to tests/acceptance/version_1/testcases/request/body/different_value_found_at_key.json diff --git a/tests/acceptance/version_1/testcases/request/body/different_value_found_at_key.py b/tests/acceptance/version_1/testcases/request/body/different_value_found_at_key.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/different_value_found_at_key.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/body/matches.py b/tests/acceptance/version_1/testcases/request/body/matches.py new file mode 100644 index 0000000..2ef0bea --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/matches.py @@ -0,0 +1,14 @@ +from pact_test.either import Right +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Right diff --git a/tests/acceptance/version_1/testcases/request/body/missing index.json b/tests/acceptance/version_1/testcases/request/body/missing_index.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/body/missing index.json rename to tests/acceptance/version_1/testcases/request/body/missing_index.json diff --git a/tests/acceptance/version_1/testcases/request/body/missing_index.py b/tests/acceptance/version_1/testcases/request/body/missing_index.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/missing_index.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/body/missing key.json b/tests/acceptance/version_1/testcases/request/body/missing_key.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/body/missing key.json rename to tests/acceptance/version_1/testcases/request/body/missing_key.json diff --git a/tests/acceptance/version_1/testcases/request/body/missing_key.py b/tests/acceptance/version_1/testcases/request/body/missing_key.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/missing_key.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/body/not null found at key when null expected.json b/tests/acceptance/version_1/testcases/request/body/not_null_found_at_key_when_null_expected.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/body/not null found at key when null expected.json rename to tests/acceptance/version_1/testcases/request/body/not_null_found_at_key_when_null_expected.json diff --git a/tests/acceptance/version_1/testcases/request/body/not_null_found_at_key_when_null_expected.py b/tests/acceptance/version_1/testcases/request/body/not_null_found_at_key_when_null_expected.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/not_null_found_at_key_when_null_expected.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/body/not null found in array when null expected.json b/tests/acceptance/version_1/testcases/request/body/not_null_found_in_array_when_null_expected.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/body/not null found in array when null expected.json rename to tests/acceptance/version_1/testcases/request/body/not_null_found_in_array_when_null_expected.json diff --git a/tests/acceptance/version_1/testcases/request/body/not_null_found_in_array_when_null_expected.py b/tests/acceptance/version_1/testcases/request/body/not_null_found_in_array_when_null_expected.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/not_null_found_in_array_when_null_expected.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/body/null found at key where not null expected.json b/tests/acceptance/version_1/testcases/request/body/null_found_at_key_where_not_null_expected.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/body/null found at key where not null expected.json rename to tests/acceptance/version_1/testcases/request/body/null_found_at_key_where_not_null_expected.json diff --git a/tests/acceptance/version_1/testcases/request/body/null_found_at_key_where_not_null_expected.py b/tests/acceptance/version_1/testcases/request/body/null_found_at_key_where_not_null_expected.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/null_found_at_key_where_not_null_expected.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/body/null found in array when not null expected.json b/tests/acceptance/version_1/testcases/request/body/null_found_in_array_when_not_null_expected.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/body/null found in array when not null expected.json rename to tests/acceptance/version_1/testcases/request/body/null_found_in_array_when_not_null_expected.json diff --git a/tests/acceptance/version_1/testcases/request/body/null_found_in_array_when_not_null_expected.py b/tests/acceptance/version_1/testcases/request/body/null_found_in_array_when_not_null_expected.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/null_found_in_array_when_not_null_expected.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/body/number found at key when string expected.json b/tests/acceptance/version_1/testcases/request/body/number_found_at_key_when_string_expected.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/body/number found at key when string expected.json rename to tests/acceptance/version_1/testcases/request/body/number_found_at_key_when_string_expected.json diff --git a/tests/acceptance/version_1/testcases/request/body/number_found_at_key_when_string_expected.py b/tests/acceptance/version_1/testcases/request/body/number_found_at_key_when_string_expected.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/number_found_at_key_when_string_expected.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/body/number found in array when string expected.json b/tests/acceptance/version_1/testcases/request/body/number_found_in_array_when_string_expected.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/body/number found in array when string expected.json rename to tests/acceptance/version_1/testcases/request/body/number_found_in_array_when_string_expected.json diff --git a/tests/acceptance/version_1/testcases/request/body/number_found_in_array_when_string_expected.py b/tests/acceptance/version_1/testcases/request/body/number_found_in_array_when_string_expected.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/number_found_in_array_when_string_expected.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/body/plain text that does not match.json b/tests/acceptance/version_1/testcases/request/body/plain_text_that_does_not_match.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/body/plain text that does not match.json rename to tests/acceptance/version_1/testcases/request/body/plain_text_that_does_not_match.json diff --git a/tests/acceptance/version_1/testcases/request/body/plain_text_that_does_not_match.py b/tests/acceptance/version_1/testcases/request/body/plain_text_that_does_not_match.py new file mode 100644 index 0000000..bd5ee83 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/plain_text_that_does_not_match.py @@ -0,0 +1,17 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + actual.headers = [{'Content-Type': 'text/plain'}] + expected.headers = [{'Content-Type': 'text/plain'}] + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/body/plain text that matches.json b/tests/acceptance/version_1/testcases/request/body/plain_text_that_matches.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/body/plain text that matches.json rename to tests/acceptance/version_1/testcases/request/body/plain_text_that_matches.json diff --git a/tests/acceptance/version_1/testcases/request/body/plain_text_that_matches.py b/tests/acceptance/version_1/testcases/request/body/plain_text_that_matches.py new file mode 100644 index 0000000..5a6ec44 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/plain_text_that_matches.py @@ -0,0 +1,17 @@ +from pact_test.either import Right +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + actual.headers = [{'Content-Type': 'text/plain'}] + expected.headers = [{'Content-Type': 'text/plain'}] + + test_result = match(actual, expected) + assert type(test_result) is Right diff --git a/tests/acceptance/version_1/testcases/request/body/string found at key when number expected.json b/tests/acceptance/version_1/testcases/request/body/string_found_at_key_when_number_expected.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/body/string found at key when number expected.json rename to tests/acceptance/version_1/testcases/request/body/string_found_at_key_when_number_expected.json diff --git a/tests/acceptance/version_1/testcases/request/body/string_found_at_key_when_number_expected.py b/tests/acceptance/version_1/testcases/request/body/string_found_at_key_when_number_expected.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/string_found_at_key_when_number_expected.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/body/string found in array when number expected.json b/tests/acceptance/version_1/testcases/request/body/string_found_in_array_when_number_expected.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/body/string found in array when number expected.json rename to tests/acceptance/version_1/testcases/request/body/string_found_in_array_when_number_expected.json diff --git a/tests/acceptance/version_1/testcases/request/body/string_found_in_array_when_number_expected.py b/tests/acceptance/version_1/testcases/request/body/string_found_in_array_when_number_expected.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/string_found_in_array_when_number_expected.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/body/unexpected index with not null value.json b/tests/acceptance/version_1/testcases/request/body/unexpected_index_with_not_null_value.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/body/unexpected index with not null value.json rename to tests/acceptance/version_1/testcases/request/body/unexpected_index_with_not_null_value.json diff --git a/tests/acceptance/version_1/testcases/request/body/unexpected_index_with_not_null_value.py b/tests/acceptance/version_1/testcases/request/body/unexpected_index_with_not_null_value.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/unexpected_index_with_not_null_value.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/body/unexpected index with null value.json b/tests/acceptance/version_1/testcases/request/body/unexpected_index_with_null_value.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/body/unexpected index with null value.json rename to tests/acceptance/version_1/testcases/request/body/unexpected_index_with_null_value.json diff --git a/tests/acceptance/version_1/testcases/request/body/unexpected_index_with_null_value.py b/tests/acceptance/version_1/testcases/request/body/unexpected_index_with_null_value.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/unexpected_index_with_null_value.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/body/unexpected key with not null value.json b/tests/acceptance/version_1/testcases/request/body/unexpected_key_with_not_null_value.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/body/unexpected key with not null value.json rename to tests/acceptance/version_1/testcases/request/body/unexpected_key_with_not_null_value.json diff --git a/tests/acceptance/version_1/testcases/request/body/unexpected_key_with_not_null_value.py b/tests/acceptance/version_1/testcases/request/body/unexpected_key_with_not_null_value.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/unexpected_key_with_not_null_value.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left diff --git a/tests/acceptance/version_1/testcases/request/body/unexpected key with null value.json b/tests/acceptance/version_1/testcases/request/body/unexpected_key_with_null_value.json similarity index 100% rename from tests/acceptance/version_1/testcases/request/body/unexpected key with null value.json rename to tests/acceptance/version_1/testcases/request/body/unexpected_key_with_null_value.json diff --git a/tests/acceptance/version_1/testcases/request/body/unexpected_key_with_null_value.py b/tests/acceptance/version_1/testcases/request/body/unexpected_key_with_null_value.py new file mode 100644 index 0000000..b37da56 --- /dev/null +++ b/tests/acceptance/version_1/testcases/request/body/unexpected_key_with_null_value.py @@ -0,0 +1,14 @@ +from pact_test.either import Left +from pact_test.models.request import PactRequest +from pact_test.matchers.request_matcher import match +from tests.acceptance.acceptance_test_loader import load_acceptance_test + + +def test(): + data = load_acceptance_test(__file__) + + actual = PactRequest(**data['actual']) + expected = PactRequest(**data['expected']) + + test_result = match(actual, expected) + assert type(test_result) is Left From bc2add0a5dbce2180e991538864319120613d9b9 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Wed, 29 Nov 2017 18:51:14 +1100 Subject: [PATCH 79/85] Fix for headers --- pact_test/clients/http_client.py | 10 +++++++--- setup.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pact_test/clients/http_client.py b/pact_test/clients/http_client.py index 173a6a7..34d25bf 100644 --- a/pact_test/clients/http_client.py +++ b/pact_test/clients/http_client.py @@ -1,3 +1,4 @@ +import json import requests from pact_test.either import * from pact_test.constants import * @@ -8,7 +9,9 @@ def execute_interaction_request(url, port, interaction): url = _build_url(url, port, interaction) method = interaction[REQUEST].get('method', 'GET') - server_response = _server_response(method, url=url) + body = interaction[REQUEST].get('body', {}) + headers = interaction[REQUEST].get('headers', {}) + server_response = _server_response(method, url=url, body=body, headers=headers) if type(server_response) is Right: headers = _parse_headers(server_response.value) @@ -23,9 +26,10 @@ def execute_interaction_request(url, port, interaction): return server_response -def _server_response(method, url): +def _server_response(method, url, body, headers): try: - return Right(requests.request(method, url=url)) + payload = json.dumps(body) + return Right(requests.request(method, url=url, data=payload, headers=headers)) except Exception as e: return Left(str(e)) diff --git a/setup.py b/setup.py index 57fd9a4..7053844 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.3.80', + version='0.3.90', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), From 68220a3c3a0b8f40c170f4d6b10164417132ad3d Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sun, 10 Dec 2017 15:00:56 +1100 Subject: [PATCH 80/85] WIP --- .travis.yml | 7 ++---- .../runners/service_providers/request_test.py | 2 +- requirements.txt | 1 + setup.cfg | 2 +- setup.py | 2 +- tests/models/service_provider_test.py | 10 ++++++++ tox.ini | 23 ------------------- 7 files changed, 16 insertions(+), 31 deletions(-) delete mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml index fd00d86..568ca67 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,11 +10,8 @@ python: - 3.6 install: - - pip install tox-travis - - pip install pytest-cov - - pip install coveralls - pip install -e . + - pip install -r requirements.txt -script: tox +script: pytest -after_success: coveralls diff --git a/pact_test/runners/service_providers/request_test.py b/pact_test/runners/service_providers/request_test.py index 0595df6..0a8619d 100644 --- a/pact_test/runners/service_providers/request_test.py +++ b/pact_test/runners/service_providers/request_test.py @@ -33,7 +33,7 @@ def verify_request(decorated_method, port=9999): if type(matching_result) is Right: resp = mock_response.__dict__ - resp['body'] = json.loads(mock_response.__dict__['body']) + resp['body'] = mock_response.__dict__['body'] out = { 'status': 'PASSED', 'providerState': decorated_method.given, diff --git a/requirements.txt b/requirements.txt index 5620863..d9d4b8c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ requests pytest>=3.0 pytest-pep8 +pytest-cov pytest-mock pytest-sugar pytest-runner diff --git a/setup.cfg b/setup.cfg index 81428bc..6dd0a4b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ universal=1 test=pytest [tool:pytest] -addopts=--pep8 --maxfail=1 -rf +addopts=--pep8 --maxfail=1 -rf --cov-report term-missing --cov=pact_test tests/ testpaths=tests python_files=*.py norecursedirs=tests/resources diff --git a/setup.py b/setup.py index 7053844..62d8ed6 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.3.90', + version='0.3.91', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), diff --git a/tests/models/service_provider_test.py b/tests/models/service_provider_test.py index 1e6c629..67b10ce 100644 --- a/tests/models/service_provider_test.py +++ b/tests/models/service_provider_test.py @@ -5,6 +5,7 @@ from pact_test import service_consumer from pact_test import will_respond_with from pact_test import ServiceProviderTest +from pact_test.either import * def test_default_service_consumer_value(): @@ -96,3 +97,12 @@ class MyTest(ServiceProviderTest): msg = 'Missing setup for "has_pact_with"' assert MyTest().is_valid().value.startswith(msg) + + +def test_valid_test(): + @service_consumer('Spam') + @has_pact_with('Eggs') + class MyTest(ServiceProviderTest): + pass + + assert type(MyTest().is_valid()) is Right diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 9d0ed3e..0000000 --- a/tox.ini +++ /dev/null @@ -1,23 +0,0 @@ -[tox] -envlist = py27, py36 - -[testenv] -deps = - pytest - pytest-pep8 - pytest-sugar - pytest-mock - pytest-cov - coveralls -passenv = - CI - TOXENV - TRAVIS - TRAVIS_BUILD_ID - TRAVIS_BRANCH - TRAVIS_JOB_NUMBER - TRAVIS_PULL_REQUEST - TRAVIS_JOB_ID - TRAVIS_REPO_SLUG - TRAVIS_COMMIT -commands = py.test --cov pact_test --cov-report term-missing -p no:sugar From 28a7db755c8779fdf2ccbeb91f3bafe1fda476c3 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Thu, 14 Dec 2017 18:30:35 +1100 Subject: [PATCH 81/85] JSON JSON JSON --- pact_test/matchers/request_matcher.py | 8 ++++---- pact_test/servers/mock_server.py | 14 +++++++------- pact_test/utils/pact_helper_utils.py | 24 +++++++++++++----------- setup.py | 2 +- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/pact_test/matchers/request_matcher.py b/pact_test/matchers/request_matcher.py index 3b9ef0e..d44abcb 100644 --- a/pact_test/matchers/request_matcher.py +++ b/pact_test/matchers/request_matcher.py @@ -1,10 +1,10 @@ from pact_test.either import * from pact_test.utils.logger import * from pact_test.matchers.matcher import * -try: - import urllib.parse as parse -except ImportError: - import urllib as parse +try: # pragma: no cover + import urllib.parse as parse # pragma: no cover +except ImportError: # pragma: no cover + import urllib as parse # pragma: no cover def match(actual, expected): diff --git a/pact_test/servers/mock_server.py b/pact_test/servers/mock_server.py index 0d4f7b5..7ac9b9a 100644 --- a/pact_test/servers/mock_server.py +++ b/pact_test/servers/mock_server.py @@ -1,12 +1,12 @@ import json from threading import Thread from pact_test.models.response import PactResponse -try: - import socketserver as SocketServer - import http.server as SimpleHTTPServer -except ImportError: - import SocketServer - import SimpleHTTPServer +try: # pragma: no cover + import socketserver as SocketServer # pragma: no cover + import http.server as SimpleHTTPServer # pragma: no cover +except ImportError: # pragma: no cover + import SocketServer # pragma: no cover + import SimpleHTTPServer # pragma: no cover ARCHIVE = [] @@ -64,7 +64,7 @@ def respond(self): for key, value in header.items(): self.send_header(key, value) self.end_headers() - self.wfile.write(str(mock_response.body).encode()) + self.wfile.write(str(mock_response.body).replace("'", '"').encode()) return Proxy diff --git a/pact_test/utils/pact_helper_utils.py b/pact_test/utils/pact_helper_utils.py index 92919f4..96417ba 100644 --- a/pact_test/utils/pact_helper_utils.py +++ b/pact_test/utils/pact_helper_utils.py @@ -49,15 +49,17 @@ def _path_to_pact_helper(consumer_tests_path): def format_headers(pact): for interaction in pact.get('interactions', []): - req_headers = interaction.get('request').get('headers') - fixed_req_headers = {} - for h in req_headers: - fixed_req_headers.update(h) - interaction['request']['headers'] = fixed_req_headers - - res_headers = interaction.get('response').get('headers') - fixes_req_headers = {} - for h in res_headers: - fixes_req_headers.update(h) - interaction['response']['headers'] = fixes_req_headers + if interaction.get('request') is not None: + req_headers = interaction.get('request').get('headers') + fixed_req_headers = {} + for h in req_headers: + fixed_req_headers.update(h) + interaction['request']['headers'] = fixed_req_headers + + if interaction.get('response') is not None: + res_headers = interaction.get('response').get('headers') + fixes_req_headers = {} + for h in res_headers: + fixes_req_headers.update(h) + interaction['response']['headers'] = fixes_req_headers return pact diff --git a/setup.py b/setup.py index 62d8ed6..8f45b02 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.3.91', + version='0.3.102', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(), From d0ea30b35cd30398317e27a725108c3031c0af62 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 16 Dec 2017 15:44:25 +1100 Subject: [PATCH 82/85] Pact broker errors --- tests/repositories/pact_broker.py | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/repositories/pact_broker.py b/tests/repositories/pact_broker.py index a3fc33f..81cccbf 100644 --- a/tests/repositories/pact_broker.py +++ b/tests/repositories/pact_broker.py @@ -6,6 +6,46 @@ from pact_test.repositories.pact_broker import get_latest_version +def test_current_version_error(mocker): + + class GetResponse(object): + status_code = 200 + + def json(self): + raise requests.exceptions.ConnectionError('Boom!') + + mocker.patch.object(requests, 'put', lambda x, **kwargs: + PutResponse()) + mocker.patch.object(requests, 'get', lambda x, **kwargs: + GetResponse()) + + out = upload_pact('provider', 'consumer', {}) + assert type(out) is Left + + +def test_connection_error(mocker): + + class GetResponse(object): + status_code = 200 + + def json(self): + return {'_embedded': {'versions': [{'number': '1.0.41'}]}} + + class PutResponse(object): + status_code = 200 + + def json(self): + raise requests.exceptions.ConnectionError('Boom!') + + mocker.patch.object(requests, 'put', lambda x, **kwargs: + PutResponse()) + mocker.patch.object(requests, 'get', lambda x, **kwargs: + GetResponse()) + + out = upload_pact('provider', 'consumer', {}) + assert type(out) is Left + + def test_upload_pact(mocker): class GetResponse(object): status_code = 200 From cc9ac38376a91cb055cf4c60cac80120db36c42d Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 23 Dec 2017 14:32:51 +1100 Subject: [PATCH 83/85] Voverage setup --- .coveragerc | 3 +++ setup.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..1006d85 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[run] +omit = pact_test/utils/logger.py + diff --git a/setup.py b/setup.py index 8f45b02..55dfa2d 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,9 @@ 'pytest>=3.0', 'pytest-pep8', 'pytest-sugar', - 'pytest-mock' + 'pytest-mock', + 'pytest-cov', + 'pytest-runner' ], url='https://github.com/Kalimaha/pact-test/', scripts=['bin/pact-test'], From f2bb670ceb7976463efb27fb23b1dd13c11ff906 Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Sat, 23 Dec 2017 15:36:01 +1100 Subject: [PATCH 84/85] Increase covergae for Mock Server --- tests/servers/mock_server.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/servers/mock_server.py b/tests/servers/mock_server.py index c98bfa1..5c1af2e 100644 --- a/tests/servers/mock_server.py +++ b/tests/servers/mock_server.py @@ -1,7 +1,22 @@ import requests +from pact_test.models.response import PactResponse from pact_test.servers.mock_server import MockServer +def test_response_with_headers(): + r = PactResponse(headers=[{'spam': 'eggs'}]) + s = MockServer(port=1234, mock_response=r) + s.start() + response = requests.get('http://localhost:1234/') + s.shutdown() + stored_request = s.report()[0] + custom_header = False + for h in response.headers: + if 'spam' in h: + custom_header = True + assert custom_header is True + + def test_no_requests(): s = MockServer() s.start() From c697c9c5542353ddc27d368a43691d3b45dc06fc Mon Sep 17 00:00:00 2001 From: Kalimaha Date: Thu, 18 Jan 2018 11:40:33 +1100 Subject: [PATCH 85/85] Fixed README --- README.rst | 64 +++++++++++++++++++++++++++++++++--------------------- setup.py | 2 +- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/README.rst b/README.rst index 15cdc25..ef7ab84 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,17 @@ Consumer Driven Contracts Testing. For further information about Pact project, c useful resources please refer to the `Pact website `_. There are two phases in Consumer Driven Contracts Testing: a Consumer sets up a contract (*it's consumer driven -after all!*), and a Provider honours it. +after all!*), and a Provider honours it. But, before that... + +Installation +~~~~~~~~~~~~ + +Pact Test is distributed through `PyPi `_ so it can be easily included in the +:code:`requirements.txt` file or normally installed with :code:`pip`: + +.. code:: bash + + $ pip install pact-test Providers Tests (*Set the Contracts*) ------------------------------------- @@ -44,21 +54,35 @@ all the interactions with their providers in the following way: .. code:: python - @service_consumer('UberEats') - @has_pact_with('Dominos Pizza') - class DominosPizzaTest(ServiceProviderTest): + @service_consumer('PythonEats') + @has_pact_with('PyzzaHut') + class PyzzaHutTest(ServiceProviderTest): - @given('some pizza exist') - @upon_receiving('a request for an hawaiian pizza') - @with_request({'method': 'get', 'path': '/pizzas/hawaiian/'}) - @will_respond_with({'status': 404, 'body': json.dumps({'reason': 'we do not serve pineapple with pizza'})}) - def test_get_pizza(self): - pizza = get_pizza('hawaiian') - assert pizza.status_code == 404 + @given('some pizzas exist') + @upon_receiving('a request for a pepperoni pizza') + @with_request({'method': 'get', 'path': '/pizzas/pepperoni/'}) + @will_respond_with({'status': 200, 'body': {'id': 42, 'type': 'pepperoni'}}) + def test_get_pepperoni_pizza(self): + pizza = get_pizza('pepperoni') + assert pizza['id'] == 42 + assert pizza['type'] == 'pepperoni' This test verifies, against a mock server, the expected interaction and creates a JSON file (*the pact*) that will be stored locally and also sent to the -Pact Broker, if available. +Pact Broker, if available. It is possible to define multiple tests for the same +state in order to verify all the scenarios of interest, For example, we can +test an unhappy :code:`404` situation: + +.. code:: python + + @given('some pizzas exist') + @upon_receiving('a request for an hawaiian pizza') + @with_request({'method': 'get', 'path': '/pizzas/hawaiian/'}) + @will_respond_with({'status': 404, 'body': {'message': 'we do not serve pineapple with pizza'}}) + def test_get_hawaiian_pizza(self): + pizza = get_pizza('hawaiian') + assert pizza.status_code == 404 + assert pizza.json()['message'] == 'we do not serve pineapple with pizza' Consumers Tests (*Honour Your Contracts*) ----------------------------------------- @@ -83,16 +107,6 @@ of an hypothetical restaurant service implemented with the most popular Python w There are few things required to setup and run consumer tests. -Installation -~~~~~~~~~~~~ - -Pact Test is distributed through `PyPi `_ so it can be easily included in the -:code:`requirements.txt` file or normally installed with :code:`pip`: - -.. code:: bash - - $ pip install pact-test - Pact Helper ~~~~~~~~~~~ @@ -171,14 +185,14 @@ Development =========== Setup ------ +~~~~~ .. code:: bash python3 setup.py install Test ----- +~~~~ It is possible to run the tests locally with Docker through the following command: @@ -200,7 +214,7 @@ possible to test all the versions at once with: $ ./bin/test all Upload New Version ------------------- +~~~~~~~~~~~~~~~~~~ .. code:: bash diff --git a/setup.py b/setup.py index 55dfa2d..9f94e74 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='pact-test', - version='0.3.102', + version='1.0.3', author='Guido Barbaglia', author_email='guido.barbaglia@gmail.com', packages=find_packages(),