Skip to content

Commit

Permalink
feat: added support for message provider (#251)
Browse files Browse the repository at this point in the history
* feat: message provider implementation

* feat: add basic verify flow

* feat: add missing flask config

* feat: initial flask start setup

* feat: and sample provider test, rename MessageProvider constructor

* feat: move handler to provider

* feat: pass handler as python argument

* feat: pass handler as python argument

* feat: create setup endpoint for message handlers, add setup_state fn, refactor test

* feat: enable context manager in message provider, allow provider to pass Proxy port param, new endpoints to stop/check proxy

* fix: flake8

* fix: revert bad merge to http_proxy; add pydocstyle

* feat: parse content, update readme and test

* test: add missing tests for message provider

* fix: check the pact files exists before running the vefivication

* fix: flake8

* feat: rebase feature from master branch

* fix: remove dead code

* feat: add http_proxy test, replace print with log, use flask localstack to store states

* fix: change PROXY_PORT to 1234 to fix broken build

* fix: flake8

* chore: skip provider test to make the build pass (troubleshooting)

* chore: skip 2 tests that causes BrokenPipeError for investigation

* chore: comment out the broken tests

* fix: change default proxy port to 1234

* feat: updated message consumer test to be compatible with message provider test

* fix: Updated tests that where failing on master branch as well

* feat: ported message_provider to fastapi

* fix: fixed comment

* fix: removed unnecessary pact file updated flask

* fix: added detectcontentlambda-contentprovider.json to .gitignore

Co-authored-by: Tuan Pham <q.tuan.p@gmail.com>
Co-authored-by: William Infante <williaminfante@gmail.com>
  • Loading branch information
3 people authored Jul 31, 2021
1 parent 2c81029 commit 903371b
Show file tree
Hide file tree
Showing 16 changed files with 535 additions and 44 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# pact-python specific ignores
e2e/pacts
userserviceclient-userservice.json
detectcontentlambda-contentprovider.json
pact/bin

# Byte-compiled / optimized / DLL files
Expand Down
6 changes: 2 additions & 4 deletions examples/message/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class MessageHandler(object):
```

Below is a snippet from a test where the message handler has no error.
Since the expected event contains a key `documentType` with value `microsoft-word`, message handler does not throw an error and a pact file `f"pacts/{expected_json}"` is expected to be generated.
Since the expected event contains a key `documentType` with value `microsoft-word`, message handler does not throw an error and a pact file `f"{PACT_FILE}""` is expected to be generated.

```python
def test_generate_new_pact_file(pact):
Expand Down Expand Up @@ -69,7 +69,7 @@ def test_generate_new_pact_file(pact):
assert isfile(f"{PACT_FILE}") == 1
```

For a similar test where the event does not contain a key `documentType` with value `microsoft-word`, a `CustomError` is generated and there there is no generated json file `f"pacts/{expected_json}"`.
For a similar test where the event does not contain a key `documentType` with value `microsoft-word`, a `CustomError` is generated and there there is no generated json file `f"{PACT_FILE}"`.

```python
def test_throw_exception_handler(pact):
Expand Down Expand Up @@ -97,8 +97,6 @@ def test_throw_exception_handler(pact):
assert isfile(f"{PACT_FILE}") == 0
```

Otherwise, no pact file is generated.

## Provider

Note: The current example only tests the consumer side.
Expand Down
64 changes: 34 additions & 30 deletions examples/message/tests/consumer/test_message_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@
PACT_BROKER_URL = "http://localhost"
PACT_BROKER_USERNAME = "pactbroker"
PACT_BROKER_PASSWORD = "pactbroker"
PACT_DIR = 'pacts'
PACT_DIR = "pacts"

CONSUMER_NAME = 'DetectContentLambda'
PROVIDER_NAME = 'ContentProvider'
PACT_FILE = (f"{PACT_DIR}/{CONSUMER_NAME.lower().replace(' ', '_')}_message-"
+ f"{PROVIDER_NAME.lower().replace(' ', '_')}_message.json")
CONSUMER_NAME = "DetectContentLambda"
PROVIDER_NAME = "ContentProvider"
PACT_FILE = (f"{PACT_DIR}/{CONSUMER_NAME.lower().replace(' ', '_')}-"
+ f"{PROVIDER_NAME.lower().replace(' ', '_')}.json")

@pytest.fixture(scope='session')

@pytest.fixture(scope="session")
def pact(request):
version = request.config.getoption('--publish-pact')
version = request.config.getoption("--publish-pact")
publish = True if version else False

pact = MessageConsumer(CONSUMER_NAME, version=version).has_pact_with(
Expand Down Expand Up @@ -54,57 +55,59 @@ def progressive_delay(file, time_to_wait=10, second_interval=0.5, verbose=False)
time.sleep(second_interval)
time_counter += 1
if verbose:
print(f'Trying for {time_counter*second_interval} seconds')
print(f"Trying for {time_counter*second_interval} seconds")
if time_counter > time_to_wait:
if verbose:
print(f'Already waited {time_counter*second_interval} seconds')
print(f"Already waited {time_counter*second_interval} seconds")
break


def test_throw_exception_handler(pact):
cleanup_json(PACT_FILE)

wrong_event = {
'documentName': 'spreadsheet.xls',
'creator': 'WI',
'documentType': 'microsoft-excel'
"event": "ObjectCreated:Put",
"documentName": "spreadsheet.xls",
"creator": "WI",
"documentType": "microsoft-excel"
}

(pact
.given('Another document in Document Service')
.expects_to_receive('Description')
.given("Document unsupported type")
.expects_to_receive("Description")
.with_content(wrong_event)
.with_metadata({
'Content-Type': 'application/json'
"Content-Type": "application/json"
}))

with pytest.raises(CustomError):
with pact:
# handler needs 'documentType' == 'microsoft-word'
# handler needs "documentType" == "microsoft-word"
MessageHandler(wrong_event)

progressive_delay(f"{PACT_FILE}")
assert isfile(f"{PACT_FILE}") == 0


def test_generate_new_pact_file(pact):
def test_put_file(pact):
cleanup_json(PACT_FILE)

expected_event = {
'documentName': 'document.doc',
'creator': 'TP',
'documentType': 'microsoft-word'
"event": "ObjectCreated:Put",
"documentName": "document.doc",
"creator": "TP",
"documentType": "microsoft-word"
}

(pact
.given('A document create in Document Service')
.expects_to_receive('Description')
.given("A document created successfully")
.expects_to_receive("Description")
.with_content(expected_event)
.with_metadata({
'Content-Type': 'application/json'
"Content-Type": "application/json"
}))

with pact:
# handler needs 'documentType' == 'microsoft-word'
MessageHandler(expected_event)

progressive_delay(f"{PACT_FILE}")
Expand All @@ -121,17 +124,18 @@ def test_publish_to_broker(pact):
`pytest tests/consumer/test_message_consumer.py::test_publish_pact_to_broker --publish-pact 2`
"""
expected_event = {
'documentName': 'document.doc',
'creator': 'TP',
'documentType': 'microsoft-word'
"event": "ObjectCreated:Delete",
"documentName": "document.doc",
"creator": "TP",
"documentType": "microsoft-word"
}

(pact
.given('A document create in Document Service with broker')
.expects_to_receive('Description with broker')
.given("A document deleted successfully")
.expects_to_receive("Description with broker")
.with_content(expected_event)
.with_metadata({
'Content-Type': 'application/json'
"Content-Type": "application/json"
}))

with pact:
Expand Down
Empty file.
51 changes: 51 additions & 0 deletions examples/message/tests/provider/test_message_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import pytest
from pact import MessageProvider


def document_created_handler():
return {
"event": "ObjectCreated:Put",
"documentName": "document.doc",
"creator": "TP",
"documentType": "microsoft-word"
}


def document_deleted_handler():
return {
"event": "ObjectCreated:Delete",
"documentName": "document.doc",
"creator": "TP",
"documentType": "microsoft-word"
}


def test_verify_success():
provider = MessageProvider(
message_providers={
'A document created successfully': document_created_handler,
'A document deleted successfully': document_deleted_handler
},
provider='ContentProvider',
consumer='DetectContentLambda',
pact_dir='pacts'

)
with provider:
provider.verify()


def test_verify_failure_when_a_provider_missing():
provider = MessageProvider(
message_providers={
'A document created successfully': document_created_handler,
},
provider='ContentProvider',
consumer='DetectContentLambda',
pact_dir='pacts'

)

with pytest.raises(AssertionError):
with provider:
provider.verify()
6 changes: 4 additions & 2 deletions pact/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
from .broker import Broker
from .consumer import Consumer
from .matchers import EachLike, Like, SomethingLike, Term, Format
from .message_pact import MessagePact
from .message_consumer import MessageConsumer
from .message_pact import MessagePact
from .message_provider import MessageProvider
from .pact import Pact
from .provider import Provider
from .verifier import Verifier

from .__version__ import __version__ # noqa: F401

__all__ = ('Broker', 'Consumer', 'EachLike', 'Like', 'MessageConsumer', 'MessagePact',
__all__ = ('Broker', 'Consumer', 'EachLike', 'Like',
'MessageConsumer', 'MessagePact', 'MessageProvider',
'Pact', 'Provider', 'SomethingLike', 'Term', 'Format', 'Verifier')
2 changes: 1 addition & 1 deletion pact/broker.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def publish(self, consumer_name, version, pact_dir=None,
for tag in consumer_tags:
command.extend(['-t', tag])

print(f"PactBroker command: {command}")
log.debug(f"PactBroker publish command: {command}")

publish_process = Popen(command)
publish_process.wait()
Expand Down
58 changes: 58 additions & 0 deletions pact/http_proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Http Proxy to be used as provider url in verifier."""
from fastapi import FastAPI, status, Request, HTTPException
import uvicorn as uvicorn
import logging
log = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)

app = FastAPI()
PROXY_PORT = 1234
UVICORN_LOGGING_LEVEL = "error"
items = {
"states": None
}


def _match_states(payload):
"""Match states in payload against stored message handlers."""
log.debug(f'Find handler from payload: {payload}')
handlers = items["states"]
states = handlers['messageHandlers']
log.debug(f'Setup states: {handlers}')
provider_states = payload['providerStates']

for state in provider_states:
matching_state = state['name']
if matching_state in states:
return states[matching_state]
raise HTTPException(status_code=500, detail='No matched handler.')


@app.post("/")
async def root(request: Request):
"""Match states with provided message handlers."""
payload = await request.json()
message = _match_states(payload)
return {'contents': message}


@app.get('/ping', status_code=status.HTTP_200_OK)
def ping():
"""Check whether the server is available before setting up states."""
return {"ping": "pong"}


@app.post("/setup", status_code=status.HTTP_201_CREATED)
async def setup(request: Request):
"""Endpoint to setup states.
Use localstack to store payload.
"""
payload = await request.json()
items["states"] = payload
return items["states"]


def run_proxy():
"""Rub HTTP Proxy."""
uvicorn.run("pact.http_proxy:app", port=PROXY_PORT, log_level=UVICORN_LOGGING_LEVEL)
7 changes: 4 additions & 3 deletions pact/message_pact.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from .broker import Broker
from .constants import MESSAGE_PATH
from .matchers import from_term


class MessagePact(Broker):
Expand Down Expand Up @@ -136,7 +137,7 @@ def with_content(self, contents):
:rtype: Pact
"""
self._insert_message_if_complete()
self._messages[0]['contents'] = contents
self._messages[0]['contents'] = from_term(contents)
return self

def expects_to_receive(self, description):
Expand Down Expand Up @@ -165,8 +166,8 @@ def write_to_pact_file(self):
json.dumps(self._messages[0]),
"--pact-dir", self.pact_dir,
f"--pact-specification-version={self.version}",
"--consumer", f"{self.consumer.name}_message",
"--provider", f"{self.provider.name}_message",
"--consumer", f"{self.consumer.name}",
"--provider", f"{self.provider.name}",
]

self._message_process = Popen(command)
Expand Down
Loading

0 comments on commit 903371b

Please sign in to comment.