Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,7 @@ messaging:


.PHONY: examples
examples: consumer flask messaging
# TODO: Fix fastapi, to run all examples this should be: consumer flask fastapi messaging
examples: consumer flask fastapi messaging


.PHONY: package
Expand Down
55 changes: 53 additions & 2 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ The following file(s) will be created when the tests are run:
The Flask [Provider] example consists of a basic Flask app, with a single endpoint route.
This implements the service expected by the [consumer](#consumer).

Functionally, this provides the same service and tests as the [fastapi_provider](#fastapi_provider). Both are included to
demonstrate how Pact can be used in different environments with different technology stacks and approaches.

The [Provider] side is responsible for performing the tests to verify if it is compliant with the [Pact file] contracts
associated with it.

Expand Down Expand Up @@ -132,7 +135,55 @@ The following file(s) will be created when the tests are run

## fastapi_provider

TODO
The FastAPI [Provider] example consists of a basic FastAPI app, with a single endpoint route.
This implements the service expected by the [consumer](#consumer).

Functionally, this provides the same service and tests as the [flask_provider](#flask_provider). Both are included to
demonstrate how Pact can be used in different environments with different technology stacks and approaches.

The [Provider] side is responsible for performing the tests to verify if it is compliant with the [Pact file] contracts
associated with it.

As such, the tests use the pact-python Verifier to perform this verification. Two approaches are demonstrated:
- Testing against the [Pact broker]. Generally this is the preferred approach, see information on [Sharing Pacts].
- Testing against the [Pact file] directly. If no [Pact broker] is available you can verify against a static [Pact file].
-
### Running

To avoid package version conflicts with different applications, it is recommended to run these tests from a
[Virtual Environment]

The following commands can be run from within your [Virtual Environment], in the `examples/fastapi_provider`.

To perform the python tests:
```bash
pip install -r requirements.txt # Install the dependencies for the FastAPI example
pip install -e ../../ # Using setup.py in the pact-python root, install any pact dependencies and pact-python
./run_pytest.sh # Wrapper script to first run FastAPI, and then run the tests
```

To perform verification using CLI to verify the [Pact file] against the FastAPI [Provider] instead of the python tests:
```bash
pip install -r requirements.txt # Install the dependencies for the FastAPI example
./verify_pact.sh # Wrapper script to first run FastAPI, and then use `pact-verifier` to verify locally
```

To perform verification using CLI, but verifying the [Pact file] previously provided by a [Consumer], and publish the
results. This example requires that the [Pact broker] is already running, and the [Consumer] tests have been published
already, described in the [consumer](#consumer) section above.
```bash
pip install -r requirements.txt # Install the dependencies for the FastAPI example
./verify_pact.sh 1 # Wrapper script to first run FastAPI, and then use `pact-verifier` to verify and publish
```

### Output

The following file(s) will be created when the tests are run

| Filename | Contents |
|-------------------------------| ----------|
| fastapi_provider/log/pact.log | All Pact interactions with the FastAPI Provider. Every interaction example retrieved from the Pact Broker will be performed during the Verification test; the request/response logged here. |


## message

Expand All @@ -153,4 +204,4 @@ without a [Pact Broker].
[Pact verification]: https://docs.pact.io/getting_started/terminology#pact-verification]
[Virtual Environment]: https://docs.python.org/3/tutorial/venv.html
[Sharing Pacts]: https://docs.pact.io/getting_started/sharing_pacts/]
[How to Run a Flask Application]: https://www.twilio.com/blog/how-run-flask-application
[How to Run a Flask Application]: https://www.twilio.com/blog/how-run-flask-application
81 changes: 0 additions & 81 deletions examples/fastapi_provider/pythonclient-pythonservice.json

This file was deleted.

1 change: 1 addition & 0 deletions examples/fastapi_provider/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ fastapi==0.67.0
pytest==5.4.1
requests>=2.26.0
uvicorn>=0.14.0
testcontainers==3.3.0
15 changes: 3 additions & 12 deletions examples/fastapi_provider/run_pytest.sh
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
#!/bin/bash
set -o pipefail

uvicorn src.provider:app --host 0.0.0.0 --port 8080 & &>/dev/null
FASTAPI_PID=$!

function teardown {
echo "Tearing down FastAPI server: ${FASTAPI_PID}"
kill -9 $FLASK_PID
}
trap teardown EXIT

sleep 1

pytest
# Unlike in the Flask example, here the FastAPI service is started up as a pytest fixture. This is then including the
# main and pact routes via fastapi_provider.py to run the tests against
pytest --run-broker True --publish-pact 1
13 changes: 12 additions & 1 deletion examples/fastapi_provider/src/provider.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import logging

from fastapi import FastAPI, HTTPException, APIRouter
from fastapi.logger import logger

fakedb = {} # Use a simple dict to represent a database

logger.setLevel(logging.DEBUG)
router = APIRouter()
app = FastAPI()


@app.get("/users/{name}")
def get_user_by_name(name: str):
"""Handle requests to retrieve a single user from the simulated database.

:param name: Name of the user to "search for"
:return: The user data if found, HTTP 404 if not
"""
user_data = fakedb.get(name)
if not user_data:
logger.error(f"GET user for: '{name}', HTTP 404 not found")
raise HTTPException(status_code=404, detail="User not found")

logger.error(f"GET user for: '{name}', returning: {user_data}")
return user_data
Empty file.
104 changes: 99 additions & 5 deletions examples/fastapi_provider/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,105 @@
import pathlib
import sys
from multiprocessing import Process

import docker
import pytest
from testcontainers.compose import DockerCompose

from .pact_provider import run_server


@pytest.fixture(scope="module")
def server():
proc = Process(target=run_server, args=(), daemon=True)
proc.start()
yield proc

# Cleanup after test
if sys.version_info >= (3, 7):
# multiprocessing.kill is new in 3.7
proc.kill()
else:
proc.terminate()


def pytest_addoption(parser):
parser.addoption(
"--publish-pact", type=str, action="store",
help="Upload generated pact file to pact broker with version"
"--publish-pact", type=str, action="store", help="Upload generated pact file to pact broker with version"
)

parser.addoption(
"--provider-url", type=str, action="store",
help="The url to our provider."
parser.addoption("--run-broker", type=bool, action="store", help="Whether to run broker in this test or not.")


@pytest.fixture(scope="session", autouse=True)
def publish_existing_pact(broker):
"""Publish the contents of the pacts folder to the Pact Broker.

In normal usage, a Consumer would publish Pacts to the Pact Broker after
running tests - this fixture would NOT be needed.
.
Because the broker is being used standalone here, it will not contain the
required Pacts, so we must first spin up the pact-cli and publish them.

In the Pact Broker logs, this corresponds to the following entry:
PactBroker::Pacts::Service -- Creating new pact publication with params \
{:consumer_name=>"UserServiceClient", :provider_name=>"UserService", \
:revision_number=>nil, :consumer_version_number=>"1", :pact_version_sha=>nil, \
:consumer_name_in_pact=>"UserServiceClient", :provider_name_in_pact=>"UserService"}
"""
source = str(pathlib.Path.cwd().joinpath("..", "pacts").resolve())
pacts = [f"{source}:/pacts"]
envs = {
"PACT_BROKER_BASE_URL": "http://broker_app:9292",
"PACT_BROKER_USERNAME": "pactbroker",
"PACT_BROKER_PASSWORD": "pactbroker",
}

client = docker.from_env()

print("Publishing existing Pact")
client.containers.run(
remove=True,
network="broker_default",
volumes=pacts,
image="pactfoundation/pact-cli:latest",
environment=envs,
command="publish /pacts --consumer-app-version 1",
)
print("Finished publishing")


# This fixture is to simulate a managed Pact Broker or Pactflow account.
# For almost all purposes outside this example, you will want to use a real
# broker. See https://github.com/pact-foundation/pact_broker for further details.
@pytest.fixture(scope="session", autouse=True)
def broker(request):
version = request.config.getoption("--publish-pact")
publish = True if version else False

# If the results are not going to be published to the broker, there is
# nothing further to do anyway
if not publish:
yield
return

run_broker = request.config.getoption("--run-broker")

if run_broker:
# Start up the broker using docker-compose
print("Starting broker")
with DockerCompose("../broker", compose_file_name=["docker-compose.yml"], pull=True) as compose:
stdout, stderr = compose.get_logs()
if stderr:
print("Errors\\n:{}".format(stderr))
print("{}".format(stdout))
print("Started broker")

yield
print("Stopping broker")
print("Broker stopped")
else:
# Assuming there is a broker available already, docker-compose has been
# used manually as the --run-broker option has not been provided
yield
return
50 changes: 0 additions & 50 deletions examples/fastapi_provider/tests/fastapi_provider.py

This file was deleted.

Loading