Skip to content

Commit 9c6355a

Browse files
pulphixtuan-phamwilliaminfante
authored andcommitted
feat: added support for message provider (pact-foundation#251)
* 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 <[email protected]> Co-authored-by: William Infante <[email protected]>
1 parent 333c9e8 commit 9c6355a

16 files changed

+535
-44
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# pact-python specific ignores
22
e2e/pacts
33
userserviceclient-userservice.json
4+
detectcontentlambda-contentprovider.json
45
pact/bin
56

67
# Byte-compiled / optimized / DLL files

examples/message/README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class MessageHandler(object):
4141
```
4242

4343
Below is a snippet from a test where the message handler has no error.
44-
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.
44+
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.
4545

4646
```python
4747
def test_generate_new_pact_file(pact):
@@ -69,7 +69,7 @@ def test_generate_new_pact_file(pact):
6969
assert isfile(f"{PACT_FILE}") == 1
7070
```
7171

72-
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}"`.
72+
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}"`.
7373

7474
```python
7575
def test_throw_exception_handler(pact):
@@ -97,8 +97,6 @@ def test_throw_exception_handler(pact):
9797
assert isfile(f"{PACT_FILE}") == 0
9898
```
9999

100-
Otherwise, no pact file is generated.
101-
102100
## Provider
103101

104102
Note: The current example only tests the consumer side.

examples/message/tests/consumer/test_message_consumer.py

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,17 @@
1616
PACT_BROKER_URL = "http://localhost"
1717
PACT_BROKER_USERNAME = "pactbroker"
1818
PACT_BROKER_PASSWORD = "pactbroker"
19-
PACT_DIR = 'pacts'
19+
PACT_DIR = "pacts"
2020

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

26-
@pytest.fixture(scope='session')
26+
27+
@pytest.fixture(scope="session")
2728
def pact(request):
28-
version = request.config.getoption('--publish-pact')
29+
version = request.config.getoption("--publish-pact")
2930
publish = True if version else False
3031

3132
pact = MessageConsumer(CONSUMER_NAME, version=version).has_pact_with(
@@ -54,57 +55,59 @@ def progressive_delay(file, time_to_wait=10, second_interval=0.5, verbose=False)
5455
time.sleep(second_interval)
5556
time_counter += 1
5657
if verbose:
57-
print(f'Trying for {time_counter*second_interval} seconds')
58+
print(f"Trying for {time_counter*second_interval} seconds")
5859
if time_counter > time_to_wait:
5960
if verbose:
60-
print(f'Already waited {time_counter*second_interval} seconds')
61+
print(f"Already waited {time_counter*second_interval} seconds")
6162
break
6263

6364

6465
def test_throw_exception_handler(pact):
6566
cleanup_json(PACT_FILE)
67+
6668
wrong_event = {
67-
'documentName': 'spreadsheet.xls',
68-
'creator': 'WI',
69-
'documentType': 'microsoft-excel'
69+
"event": "ObjectCreated:Put",
70+
"documentName": "spreadsheet.xls",
71+
"creator": "WI",
72+
"documentType": "microsoft-excel"
7073
}
7174

7275
(pact
73-
.given('Another document in Document Service')
74-
.expects_to_receive('Description')
76+
.given("Document unsupported type")
77+
.expects_to_receive("Description")
7578
.with_content(wrong_event)
7679
.with_metadata({
77-
'Content-Type': 'application/json'
80+
"Content-Type": "application/json"
7881
}))
7982

8083
with pytest.raises(CustomError):
8184
with pact:
82-
# handler needs 'documentType' == 'microsoft-word'
85+
# handler needs "documentType" == "microsoft-word"
8386
MessageHandler(wrong_event)
8487

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

8891

89-
def test_generate_new_pact_file(pact):
92+
def test_put_file(pact):
9093
cleanup_json(PACT_FILE)
9194

9295
expected_event = {
93-
'documentName': 'document.doc',
94-
'creator': 'TP',
95-
'documentType': 'microsoft-word'
96+
"event": "ObjectCreated:Put",
97+
"documentName": "document.doc",
98+
"creator": "TP",
99+
"documentType": "microsoft-word"
96100
}
97101

98102
(pact
99-
.given('A document create in Document Service')
100-
.expects_to_receive('Description')
103+
.given("A document created successfully")
104+
.expects_to_receive("Description")
101105
.with_content(expected_event)
102106
.with_metadata({
103-
'Content-Type': 'application/json'
107+
"Content-Type": "application/json"
104108
}))
105109

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

110113
progressive_delay(f"{PACT_FILE}")
@@ -121,17 +124,18 @@ def test_publish_to_broker(pact):
121124
`pytest tests/consumer/test_message_consumer.py::test_publish_pact_to_broker --publish-pact 2`
122125
"""
123126
expected_event = {
124-
'documentName': 'document.doc',
125-
'creator': 'TP',
126-
'documentType': 'microsoft-word'
127+
"event": "ObjectCreated:Delete",
128+
"documentName": "document.doc",
129+
"creator": "TP",
130+
"documentType": "microsoft-word"
127131
}
128132

129133
(pact
130-
.given('A document create in Document Service with broker')
131-
.expects_to_receive('Description with broker')
134+
.given("A document deleted successfully")
135+
.expects_to_receive("Description with broker")
132136
.with_content(expected_event)
133137
.with_metadata({
134-
'Content-Type': 'application/json'
138+
"Content-Type": "application/json"
135139
}))
136140

137141
with pact:

examples/message/tests/provider/__init__.py

Whitespace-only changes.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import pytest
2+
from pact import MessageProvider
3+
4+
5+
def document_created_handler():
6+
return {
7+
"event": "ObjectCreated:Put",
8+
"documentName": "document.doc",
9+
"creator": "TP",
10+
"documentType": "microsoft-word"
11+
}
12+
13+
14+
def document_deleted_handler():
15+
return {
16+
"event": "ObjectCreated:Delete",
17+
"documentName": "document.doc",
18+
"creator": "TP",
19+
"documentType": "microsoft-word"
20+
}
21+
22+
23+
def test_verify_success():
24+
provider = MessageProvider(
25+
message_providers={
26+
'A document created successfully': document_created_handler,
27+
'A document deleted successfully': document_deleted_handler
28+
},
29+
provider='ContentProvider',
30+
consumer='DetectContentLambda',
31+
pact_dir='pacts'
32+
33+
)
34+
with provider:
35+
provider.verify()
36+
37+
38+
def test_verify_failure_when_a_provider_missing():
39+
provider = MessageProvider(
40+
message_providers={
41+
'A document created successfully': document_created_handler,
42+
},
43+
provider='ContentProvider',
44+
consumer='DetectContentLambda',
45+
pact_dir='pacts'
46+
47+
)
48+
49+
with pytest.raises(AssertionError):
50+
with provider:
51+
provider.verify()

pact/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
from .broker import Broker
33
from .consumer import Consumer
44
from .matchers import EachLike, Like, SomethingLike, Term, Format
5-
from .message_pact import MessagePact
65
from .message_consumer import MessageConsumer
6+
from .message_pact import MessagePact
7+
from .message_provider import MessageProvider
78
from .pact import Pact
89
from .provider import Provider
910
from .verifier import Verifier
1011

1112
from .__version__ import __version__ # noqa: F401
1213

13-
__all__ = ('Broker', 'Consumer', 'EachLike', 'Like', 'MessageConsumer', 'MessagePact',
14+
__all__ = ('Broker', 'Consumer', 'EachLike', 'Like',
15+
'MessageConsumer', 'MessagePact', 'MessageProvider',
1416
'Pact', 'Provider', 'SomethingLike', 'Term', 'Format', 'Verifier')

pact/broker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def publish(self, consumer_name, version, pact_dir=None,
8585
for tag in consumer_tags:
8686
command.extend(['-t', tag])
8787

88-
print(f"PactBroker command: {command}")
88+
log.debug(f"PactBroker publish command: {command}")
8989

9090
publish_process = Popen(command)
9191
publish_process.wait()

pact/http_proxy.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""Http Proxy to be used as provider url in verifier."""
2+
from fastapi import FastAPI, status, Request, HTTPException
3+
import uvicorn as uvicorn
4+
import logging
5+
log = logging.getLogger(__name__)
6+
logging.basicConfig(level=logging.DEBUG)
7+
8+
app = FastAPI()
9+
PROXY_PORT = 1234
10+
UVICORN_LOGGING_LEVEL = "error"
11+
items = {
12+
"states": None
13+
}
14+
15+
16+
def _match_states(payload):
17+
"""Match states in payload against stored message handlers."""
18+
log.debug(f'Find handler from payload: {payload}')
19+
handlers = items["states"]
20+
states = handlers['messageHandlers']
21+
log.debug(f'Setup states: {handlers}')
22+
provider_states = payload['providerStates']
23+
24+
for state in provider_states:
25+
matching_state = state['name']
26+
if matching_state in states:
27+
return states[matching_state]
28+
raise HTTPException(status_code=500, detail='No matched handler.')
29+
30+
31+
@app.post("/")
32+
async def root(request: Request):
33+
"""Match states with provided message handlers."""
34+
payload = await request.json()
35+
message = _match_states(payload)
36+
return {'contents': message}
37+
38+
39+
@app.get('/ping', status_code=status.HTTP_200_OK)
40+
def ping():
41+
"""Check whether the server is available before setting up states."""
42+
return {"ping": "pong"}
43+
44+
45+
@app.post("/setup", status_code=status.HTTP_201_CREATED)
46+
async def setup(request: Request):
47+
"""Endpoint to setup states.
48+
49+
Use localstack to store payload.
50+
"""
51+
payload = await request.json()
52+
items["states"] = payload
53+
return items["states"]
54+
55+
56+
def run_proxy():
57+
"""Rub HTTP Proxy."""
58+
uvicorn.run("pact.http_proxy:app", port=PROXY_PORT, log_level=UVICORN_LOGGING_LEVEL)

pact/message_pact.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from .broker import Broker
99
from .constants import MESSAGE_PATH
10+
from .matchers import from_term
1011

1112

1213
class MessagePact(Broker):
@@ -136,7 +137,7 @@ def with_content(self, contents):
136137
:rtype: Pact
137138
"""
138139
self._insert_message_if_complete()
139-
self._messages[0]['contents'] = contents
140+
self._messages[0]['contents'] = from_term(contents)
140141
return self
141142

142143
def expects_to_receive(self, description):
@@ -165,8 +166,8 @@ def write_to_pact_file(self):
165166
json.dumps(self._messages[0]),
166167
"--pact-dir", self.pact_dir,
167168
f"--pact-specification-version={self.version}",
168-
"--consumer", f"{self.consumer.name}_message",
169-
"--provider", f"{self.provider.name}_message",
169+
"--consumer", f"{self.consumer.name}",
170+
"--provider", f"{self.provider.name}",
170171
]
171172

172173
self._message_process = Popen(command)

0 commit comments

Comments
 (0)