From 2d9b9a7db4f823cda24f76d5f413438c22c244e7 Mon Sep 17 00:00:00 2001 From: Amit Singh Date: Thu, 12 Sep 2024 12:12:20 +0530 Subject: [PATCH] chore: add https methods; updated v2 examples --- examples/.ruff.toml | 1 + examples/src/consumer.py | 43 ++- examples/src/fastapi.py | 56 ++- examples/src/flask.py | 32 +- examples/tests/test_00_consumer.py | 81 ++++- examples/tests/test_01_provider_fastapi.py | 59 +++- examples/tests/test_01_provider_flask.py | 54 ++- examples/tests/v3/test_v3_http_consumer.py | 64 ++++ ...er.py => test_v3_http_fastapi_provider.py} | 115 +++++- .../tests/v3/test_v3_http_flask_provider.py | 334 ++++++++++++++++++ 10 files changed, 825 insertions(+), 14 deletions(-) rename examples/tests/v3/{test_v3_http_provider.py => test_v3_http_fastapi_provider.py} (65%) create mode 100644 examples/tests/v3/test_v3_http_flask_provider.py diff --git a/examples/.ruff.toml b/examples/.ruff.toml index 72322dfb1f..f4063641ce 100644 --- a/examples/.ruff.toml +++ b/examples/.ruff.toml @@ -5,6 +5,7 @@ ignore = [ "S101", # Forbid assert statements "D103", # Require docstring in public function "D104", # Require docstring in public package + "PLR2004" # Forbid Magic Numbers ] [lint.per-file-ignores] diff --git a/examples/src/consumer.py b/examples/src/consumer.py index fa260d214d..b2815d2972 100644 --- a/examples/src/consumer.py +++ b/examples/src/consumer.py @@ -21,7 +21,7 @@ from dataclasses import dataclass from datetime import datetime -from typing import Any, Dict +from typing import Any, Dict, Tuple import requests @@ -102,3 +102,44 @@ def get_user(self, user_id: int) -> User: name=data["name"], created_on=datetime.fromisoformat(data["created_on"]), ) + + def create_user( + self, user: Dict[str, Any], header: Dict[str, str] + ) -> Tuple[int, User]: + """ + Create a new user on the server. + + Args: + user: The user data to create. + header: The headers to send with the request. + + Returns: + The user data including the ID assigned by the server; Error if user exists. + """ + uri = f"{self.base_uri}/users/" + response = requests.post(uri, headers=header, json=user, timeout=5) + response.raise_for_status() + data: Dict[str, Any] = response.json() + return ( + response.status_code, + User( + id=data["id"], + name=data["name"], + created_on=datetime.fromisoformat(data["created_on"]), + ), + ) + + def delete_user(self, user_id: int) -> int: + """ + Delete a user by ID from the server. + + Args: + user_id: The ID of the user to delete. + + Returns: + The response status code. + """ + uri = f"{self.base_uri}/users/{user_id}" + response = requests.delete(uri, timeout=5) + response.raise_for_status() + return response.status_code diff --git a/examples/src/fastapi.py b/examples/src/fastapi.py index b25d3c1a8e..e522b08f85 100644 --- a/examples/src/fastapi.py +++ b/examples/src/fastapi.py @@ -19,12 +19,30 @@ from __future__ import annotations +import logging from typing import Any, Dict -from fastapi import FastAPI +from pydantic import BaseModel + +from fastapi import FastAPI, HTTPException from fastapi.responses import JSONResponse app = FastAPI() +logger = logging.getLogger(__name__) + + +class User(BaseModel): + """ + User data class. + + This class is used to represent a user in the application. It is used to + validate the incoming data and to dump the data to a dictionary. + """ + + id: int | None = None + name: str + email: str + """ As this is a simple example, we'll use a simple dict to represent a database. @@ -52,3 +70,39 @@ async def get_user_by_id(uid: int) -> JSONResponse: if not user: return JSONResponse(status_code=404, content={"error": "User not found"}) return JSONResponse(status_code=200, content=user) + + +@app.post("/users/") +async def create_new_user(user: User) -> JSONResponse: + """ + Create a new user . + + Args: + user: The user data to create + + Returns: + The status code 200 and user data if successfully created, HTTP 404 if not + """ + if user.id is not None: + raise HTTPException(status_code=400, detail="ID should not be provided.") + new_uid = len(FAKE_DB) + FAKE_DB[new_uid] = user.model_dump() + + return JSONResponse(status_code=200, content=FAKE_DB[new_uid]) + + +@app.delete("/users/{user_id}", status_code=204) +async def delete_user(user_id: int): # noqa: ANN201 + """ + Delete an existing user . + + Args: + user_id: The ID of the user to delete + + Returns: + The status code 204, HTTP 404 if not + """ + if user_id not in FAKE_DB: + raise HTTPException(status_code=404, detail="User not found") + + del FAKE_DB[user_id] diff --git a/examples/src/flask.py b/examples/src/flask.py index 31d614042a..7fa32cdeae 100644 --- a/examples/src/flask.py +++ b/examples/src/flask.py @@ -19,12 +19,14 @@ from __future__ import annotations -from typing import Any, Dict, Union +import logging +from typing import Any, Dict, Tuple, Union -from flask import Flask +from flask import Flask, Response, abort, jsonify, request -app = Flask(__name__) +logger = logging.getLogger(__name__) +app = Flask(__name__) """ As this is a simple example, we'll use a simple dict to represent a database. This would be replaced with a real database in a real application. @@ -47,7 +49,29 @@ def get_user_by_id(uid: int) -> Union[Dict[str, Any], tuple[Dict[str, Any], int] Returns: The user data if found, HTTP 404 if not """ - user = FAKE_DB.get(uid) + user = FAKE_DB.get(int(uid)) if not user: return {"error": "User not found"}, 404 return user + + +@app.route("/users/", methods=["POST"]) +def create_user() -> Tuple[Response, int]: + if request.json is None: + abort(400, description="Invalid JSON data") + + data: Dict[str, Any] = request.json + new_uid: int = len(FAKE_DB) + if new_uid in FAKE_DB: + abort(400, description="User already exists") + + FAKE_DB[new_uid] = {"id": new_uid, "name": data["name"], "email": data["email"]} + return jsonify(FAKE_DB[new_uid]), 200 + + +@app.route("/users/", methods=["DELETE"]) +def delete_user(user_id: int) -> Tuple[str, int]: + if user_id not in FAKE_DB: + abort(404, description="User not found") + del FAKE_DB[user_id] + return "", 204 # No Content status code diff --git a/examples/tests/test_00_consumer.py b/examples/tests/test_00_consumer.py index 9076785081..3a2d0f8de2 100644 --- a/examples/tests/test_00_consumer.py +++ b/examples/tests/test_00_consumer.py @@ -24,18 +24,28 @@ from yarl import URL from examples.src.consumer import User, UserConsumer -from pact import Consumer, Format, Like, Provider +from pact import Consumer, Format, Like, Provider # type: ignore[attr-defined] if TYPE_CHECKING: from pathlib import Path - from pact.pact import Pact + from pact.pact import Pact # type: ignore[import-untyped] -log = logging.getLogger(__name__) +logger = logging.getLogger(__name__) MOCK_URL = URL("http://localhost:8080") +@pytest.fixture(scope="session", autouse=True) +def _setup_pact_logging() -> None: + """ + Set up logging for the pact package. + """ + from pact.v3 import ffi + + ffi.log_to_stderr("INFO") + + @pytest.fixture def user_consumer() -> UserConsumer: """ @@ -78,7 +88,7 @@ def pact(broker: URL, pact_dir: Path) -> Generator[Pact, Any, None]: pact = consumer.has_pact_with( Provider("UserProvider"), pact_dir=pact_dir, - publish_to_broker=True, + publish_to_broker=False, # Mock service configuration host_name=MOCK_URL.host, port=MOCK_URL.port, @@ -142,3 +152,66 @@ def test_get_unknown_user(pact: Pact, user_consumer: UserConsumer) -> None: assert excinfo.value.response is not None assert excinfo.value.response.status_code == HTTPStatus.NOT_FOUND pact.verify() + + +def test_post_request_to_create_user(pact: Pact, user_consumer: UserConsumer) -> None: + """ + Test the POST request for creating a new user. + + This test defines the expected interaction for a POST request to create + a new user. It sets up the expected request and response from the provider, + including the request body and headers, and verifies that the response + status code is 200 and the response body matches the expected user data. + """ + expected: Dict[str, Any] = { + "id": 124, + "name": "Jane Doe", + "email": "jane@example.com", + "created_on": Format().iso_8601_datetime(), + } + header = {"Content-Type": "application/json"} + payload = { + "name": "Jane Doe", + "email": "jane@example.com", + "created_on": "1991-02-20T06:35:26+00:00", + } + expected_response_code: int = 200 + + ( + pact.given("create user 124") + .upon_receiving("A request to create a new user") + .with_request(method="POST", path="/users/", headers=header, body=payload) + .will_respond_with(status=200, headers=header, body=Like(expected)) + ) + + with pact: + response = user_consumer.create_user(user=payload, header=header) + assert response[0] == expected_response_code + assert response[1].id == 124 + assert response[1].name == "Jane Doe" + + pact.verify() + + +def test_delete_request_to_delete_user(pact: Pact, user_consumer: UserConsumer) -> None: + """ + Test the DELETE request for deleting a user. + + This test defines the expected interaction for a DELETE request to delete + a user. It sets up the expected request and response from the provider, + including the request body and headers, and verifies that the response + status code is 200 and the response body matches the expected user data. + """ + expected_response_code: int = 204 + ( + pact.given("delete the user 124") + .upon_receiving("a request for deleting user") + .with_request(method="DELETE", path="/users/124") + .will_respond_with(204) + ) + + with pact: + response_status_code = user_consumer.delete_user(124) + assert response_status_code == expected_response_code + + pact.verify() diff --git a/examples/tests/test_01_provider_fastapi.py b/examples/tests/test_01_provider_fastapi.py index a95b5b5f8d..99d007994f 100644 --- a/examples/tests/test_01_provider_fastapi.py +++ b/examples/tests/test_01_provider_fastapi.py @@ -35,7 +35,7 @@ from yarl import URL from examples.src.fastapi import app -from pact import Verifier +from pact import Verifier # type: ignore[import-untyped] PROVIDER_URL = URL("http://localhost:8080") @@ -68,6 +68,8 @@ async def mock_pact_provider_states( mapping = { "user 123 doesn't exist": mock_user_123_doesnt_exist, "user 123 exists": mock_user_123_exists, + "create user 124": mock_post_request_to_create_user, + "delete the user 124": mock_delete_request_to_delete_user, } return {"result": mapping[state.state]()} @@ -134,6 +136,61 @@ def mock_user_123_exists() -> None: } +def mock_post_request_to_create_user() -> None: + """ + Mock the database for the post request to create a user. + """ + import examples.src.fastapi + + examples.src.fastapi.FAKE_DB = MagicMock() + examples.src.fastapi.FAKE_DB.__len__.return_value = 124 + examples.src.fastapi.FAKE_DB.__setitem__.return_value = None + examples.src.fastapi.FAKE_DB.__getitem__.return_value = { + "id": 124, + "created_on": "1991-02-20T06:35:26+00:00", + "email": "jane@example.com", + "name": "Jane Doe", + "ip_address": "10.1.2.3", + "hobbies": ["hiking", "swimming"], + "admin": False, + } + + +def mock_delete_request_to_delete_user() -> None: + """ + Mock the database for the delete request to delete a user. + """ + import examples.src.fastapi + + db_values = { + 123: { + "id": 123, + "name": "Verna Hampton", + "email": "verna@example.com", + "created_on": "1991-02-20T06:35:26+00:00", + "ip_address": "10.1.2.3", + "hobbies": ["hiking", "swimming"], + "admin": False, + }, + 124: { + "id": 124, + "name": "Jane Doe", + "email": "jane@example.com", + "created_on": "1991-02-20T06:35:26+00:00", + "ip_address": "10.1.2.5", + "hobbies": ["running", "dancing"], + "admin": False, + }, + } + + examples.src.fastapi.FAKE_DB = MagicMock() + examples.src.fastapi.FAKE_DB.__delitem__.side_effect = ( + lambda key: db_values.__delitem__(key) + ) + examples.src.fastapi.FAKE_DB.__getitem__.side_effect = lambda key: db_values[key] + examples.src.fastapi.FAKE_DB.__contains__.side_effect = lambda key: key in db_values + + def test_against_broker(broker: URL, verifier: Verifier) -> None: """ Test the provider against the broker. diff --git a/examples/tests/test_01_provider_flask.py b/examples/tests/test_01_provider_flask.py index 58cb024585..313e706aef 100644 --- a/examples/tests/test_01_provider_flask.py +++ b/examples/tests/test_01_provider_flask.py @@ -34,7 +34,7 @@ from examples.src.flask import app from flask import request -from pact import Verifier +from pact import Verifier # type: ignore[import-untyped] PROVIDER_URL = URL("http://localhost:8080") @@ -58,6 +58,8 @@ async def mock_pact_provider_states() -> Dict[str, Union[str, None]]: mapping = { "user 123 doesn't exist": mock_user_123_doesnt_exist, "user 123 exists": mock_user_123_exists, + "create user 124": mock_post_request_to_create_user, + "delete the user 124": mock_delete_request_to_delete_user, } return {"result": mapping[request.json["state"]]()} # type: ignore[index] @@ -122,6 +124,56 @@ def mock_user_123_exists() -> None: } +def mock_post_request_to_create_user() -> None: + """ + Mock the database for the post request to create a user. + """ + import examples.src.flask + + examples.src.flask.FAKE_DB = MagicMock() + examples.src.flask.FAKE_DB.__len__.return_value = 124 + examples.src.flask.FAKE_DB.__setitem__.return_value = None + examples.src.flask.FAKE_DB.__getitem__.return_value = { + "id": 124, + "created_on": "2024-09-06T05:07:06.745719+00:00", + "email": "jane@example.com", + "name": "Jane Doe", + } + + +def mock_delete_request_to_delete_user() -> None: + """ + Mock the database for the delete request to delete a user. + """ + import examples.src.flask + + db_values = { + 123: { + "id": 123, + "name": "Verna Hampton", + "created_on": "2024-08-29T04:53:07.337793+00:00", + "ip_address": "10.1.2.3", + "hobbies": ["hiking", "swimming"], + "admin": False, + }, + 124: { + "id": 124, + "name": "Jane Doe", + "created_on": "2024-08-29T04:53:07.337793+00:00", + "ip_address": "10.1.2.5", + "hobbies": ["running", "dancing"], + "admin": False, + }, + } + + examples.src.flask.FAKE_DB = MagicMock() + examples.src.flask.FAKE_DB.__delitem__.side_effect = ( + lambda key: db_values.__delitem__(key) + ) + examples.src.flask.FAKE_DB.__getitem__.side_effect = lambda key: db_values[key] + examples.src.flask.FAKE_DB.__contains__.side_effect = lambda key: key in db_values + + def test_against_broker(broker: URL, verifier: Verifier) -> None: """ Test the provider against the broker. diff --git a/examples/tests/v3/test_v3_http_consumer.py b/examples/tests/v3/test_v3_http_consumer.py index 3546021b78..0d3448fb01 100644 --- a/examples/tests/v3/test_v3_http_consumer.py +++ b/examples/tests/v3/test_v3_http_consumer.py @@ -101,3 +101,67 @@ def test_get_non_existing_user(pact: Pact) -> None: response = requests.get(f"{srv.url}/users/2", timeout=5) assert response.status_code == expected_response_code + + +def test_post_request_to_create_user(pact: Pact) -> None: + """ + Test the POST request for creating a new user. + + This test defines the expected interaction for a POST request to create + a new user. It sets up the expected request and response from the provider, + including the request body and headers, and verifies that the response + status code is 200 and the response body matches the expected user data. + """ + expected: Dict[str, Any] = { + "id": 124, + "name": "Jane Doe", + "email": "jane@example.com", + } + header = {"Content-Type": "application/json"} + body = {"name": "Jane Doe", "email": "jane@example.com"} + expected_response_code: int = 200 + + ( + pact.upon_receiving("a request to create a new user") + .given("the specified user doesn't exist") + .with_request(method="POST", path="/users/") + .with_body(json.dumps(body)) + .with_header("Content-Type", "application/json") + .will_respond_with(status=200) + .with_body(content_type="application/json", body=json.dumps(expected)) + ) + + with pact.serve() as srv: + response = requests.post( + f"{srv.url}/users/", headers=header, json=body, timeout=5 + ) + + assert response.status_code == expected_response_code + assert response.json() == { + "id": 124, + "name": "Jane Doe", + "email": "jane@example.com", + } + + +def test_delete_request_to_delete_user(pact: Pact) -> None: + """ + Test the DELETE request for deleting a user. + + This test defines the expected interaction for a DELETE request to delete + a user. It sets up the expected request and response from the provider, + including the request body and headers, and verifies that the response + status code is 200 and the response body matches the expected user data. + """ + expected_response_code: int = 204 + ( + pact.upon_receiving("a request for deleting user") + .given("user is present in DB") + .with_request(method="DELETE", path="/users/124") + .will_respond_with(204) + ) + + with pact.serve() as srv: + response = requests.delete(f"{srv.url}/users/124", timeout=5) + + assert response.status_code == expected_response_code diff --git a/examples/tests/v3/test_v3_http_provider.py b/examples/tests/v3/test_v3_http_fastapi_provider.py similarity index 65% rename from examples/tests/v3/test_v3_http_provider.py rename to examples/tests/v3/test_v3_http_fastapi_provider.py index b7c6c72f67..9495141409 100644 --- a/examples/tests/v3/test_v3_http_provider.py +++ b/examples/tests/v3/test_v3_http_fastapi_provider.py @@ -22,7 +22,7 @@ import time from multiprocessing import Process -from typing import Callable, Dict, Union +from typing import TYPE_CHECKING, Callable, Dict, Union from unittest.mock import MagicMock import uvicorn @@ -71,11 +71,15 @@ async def mock_pact_provider_states( setup_mapping: Dict[str, Callable[[], MagicMock]] = { "user doesn't exists": mock_user_doesnt_exist, "user exists": mock_user_exists, + "the specified user doesn't exist": mock_post_request_to_create_user, + "user is present in DB": mock_delete_request_to_delete_user, } teardown_mapping: Dict[str, Callable[[], str]] = { "user doesn't exists": verify_user_doesnt_exist_mock, "user exists": verify_user_exists_mock, + "the specified user doesn't exist": verify_mock_post_request_to_create_user, + "user is present in DB": verify_mock_delete_request_to_delete_user, } if action == "setup": @@ -147,6 +151,59 @@ def mock_user_exists() -> MagicMock: return mock_db +def mock_post_request_to_create_user() -> MagicMock: + """ + Mock the database for the post request to create a user. + """ + import examples.src.fastapi + + mock_db = MagicMock() + mock_db.__len__.return_value = 124 + mock_db.__setitem__.return_value = None + mock_db.__getitem__.return_value = { + "id": 124, + "created_on": "2024-09-06T05:07:06.745719+00:00", + "email": "jane@example.com", + "name": "Jane Doe", + } + examples.src.fastapi.FAKE_DB = mock_db + return mock_db + + +def mock_delete_request_to_delete_user() -> MagicMock: + """ + Mock the database for the delete request to delete a user. + """ + import examples.src.fastapi + + db_values = { + 123: { + "id": 123, + "name": "Verna Hampton", + "created_on": "2024-08-29T04:53:07.337793+00:00", + "ip_address": "10.1.2.3", + "hobbies": ["hiking", "swimming"], + "admin": False, + }, + 124: { + "id": 124, + "name": "Jane Doe", + "created_on": "2024-08-29T04:53:07.337793+00:00", + "ip_address": "10.1.2.5", + "hobbies": ["running", "dancing"], + "admin": False, + }, + } + + mock_db = MagicMock() + mock_db.__delitem__.side_effect = lambda key: db_values.__delitem__(key) + mock_db.__getitem__.side_effect = lambda key: db_values[key] + mock_db.__contains__.side_effect = lambda key: key in db_values + examples.src.fastapi.FAKE_DB = mock_db + + return mock_db + + def verify_user_doesnt_exist_mock() -> str: """ Verify the mock calls for the 'user doesn't exist' state. @@ -161,6 +218,9 @@ def verify_user_doesnt_exist_mock() -> str: """ import examples.src.fastapi + if TYPE_CHECKING: + examples.src.fastapi.FAKE_DB = MagicMock() + examples.src.fastapi.FAKE_DB.get.assert_called() assert ( @@ -183,7 +243,7 @@ def verify_user_exists_mock() -> str: This function checks that the mock for `FAKE_DB.get` was called, verifies that it returned the expected user data, - and ensures that it was called with the integer argument `1`. + and ensures that it was called with the integer argument `123`. It then resets the mock for future tests. Returns: @@ -191,6 +251,9 @@ def verify_user_exists_mock() -> str: """ import examples.src.fastapi + if TYPE_CHECKING: + examples.src.fastapi.FAKE_DB = MagicMock() + examples.src.fastapi.FAKE_DB.get.assert_called() expected_return = { @@ -215,3 +278,51 @@ def verify_user_exists_mock() -> str: examples.src.fastapi.FAKE_DB.reset_mock() return "Verified user exists mock" + + +def verify_mock_post_request_to_create_user() -> str: + import examples.src.fastapi + + if TYPE_CHECKING: + examples.src.fastapi.FAKE_DB = MagicMock() + + examples.src.fastapi.FAKE_DB.__getitem__.assert_called() + + expected_return = { + "id": 124, + "created_on": "2024-09-06T05:07:06.745719+00:00", + "email": "jane@example.com", + "name": "Jane Doe", + } + + examples.src.fastapi.FAKE_DB.__len__.assert_called() + assert ( + examples.src.fastapi.FAKE_DB.__getitem__.return_value == expected_return + ), "Unexpected return value from __getitem__()" + + args, _ = examples.src.fastapi.FAKE_DB.__getitem__.call_args + assert isinstance( + args[0], int + ), f"Expected get() to be called with an integer, but got {type(args[0])}" + assert args[0] == 124, f"Expected get(124), but got get({args[0]})" + + examples.src.fastapi.FAKE_DB.reset_mock() + + return "Verified the post request mock" + + +def verify_mock_delete_request_to_delete_user() -> str: + import examples.src.fastapi + + if TYPE_CHECKING: + examples.src.fastapi.FAKE_DB = MagicMock() + + examples.src.fastapi.FAKE_DB.__delitem__.assert_called() + + args, _ = examples.src.fastapi.FAKE_DB.__delitem__.call_args + assert isinstance( + args[0], int + ), f"Expected __delitem__() to be called with an integer, but got {type(args[0])}" + + examples.src.fastapi.FAKE_DB.reset_mock() + return "Verified the DELETE request mock" diff --git a/examples/tests/v3/test_v3_http_flask_provider.py b/examples/tests/v3/test_v3_http_flask_provider.py new file mode 100644 index 0000000000..b92c0272c9 --- /dev/null +++ b/examples/tests/v3/test_v3_http_flask_provider.py @@ -0,0 +1,334 @@ +""" +Test the Flask provider with Pact. + +This module tests the Flask provider defined in `src/flask.py` against the mock +consumer. The mock consumer is set up by Pact and will replay the requests +defined by the consumers. Pact will then validate that the provider responds +with the expected responses. + +The provider will be expected to be in a given state in order to respond to +certain requests. For example, when fetching a user's information, the provider +will need to have a user with the given ID in the database. In order to avoid +side effects, the provider's database calls are mocked out using functionalities +from `unittest.mock`. + +In order to set the provider into the correct state, this test module defines an +additional endpoint on the provider, in this case `/_pact/provider_states`. +Calls to this endpoint mock the relevant database calls to set the provider into +the correct state. + +A good resource for understanding the provider tests is the [Pact Provider +Test](https://docs.pact.io/5-minute-getting-started-guide#scope-of-a-provider-pact-test) +section of the Pact documentation. +""" + +from __future__ import annotations + +import logging +import time +from multiprocessing import Process +from typing import TYPE_CHECKING, Callable, Dict, Union +from unittest.mock import MagicMock + +import pytest +from yarl import URL + +from examples.src.flask import app +from flask import request +from pact.v3 import Verifier + +PROVIDER_URL = URL("http://localhost:8000") + +ProviderStateResult = Union[MagicMock, str] + +logger = logging.getLogger(__name__) + + +@pytest.fixture(scope="session", autouse=True) +def _setup_pact_logging() -> None: + """ + Set up logging for the pact package. + """ + from pact.v3 import ffi + + ffi.log_to_stderr("INFO") + + +@app.route("/_pact/callback", methods=["POST"]) +def mock_pact_provider_states() -> Dict[str, str]: + """ + Define the provider state for Pact testing. + + This endpoint is used by Pact to set up or tear down the provider's state + before running tests. + It achieves this by calling predefined functions that mock or verify + the state based on the provided action and state parameters. + + Parameters: + action (str): The action to perform, either "setup" or "teardown". + state (str): The specific state to set up or tear down. + + The function uses two mappings: + `setup_mapping`: Maps state names to functions that set up the provider's state + by mocking relevant database calls or other side effects. + + `teardown_mapping`: Maps state names to functions that verify + the state has been correctly set up or cleaned up. + + Based on the `action` parameter, the function calls the corresponding function + from either `setup_mapping` or `teardown_mapping` and returns the result of that + function as a dictionary. + + """ + action = request.args.get("action") + state = request.args.get("state") + if not state: + msg = "State must be provided" + raise ValueError(msg) + + setup_mapping: Dict[str, Callable[[], None]] = { + "user doesn't exists": mock_user_doesnt_exist, + "user exists": mock_user_exists, + "the specified user doesn't exist": mock_post_request_to_create_user, + "user is present in DB": mock_delete_request_to_delete_user, + } + + teardown_mapping: Dict[str, Callable[[], None]] = { + "user doesn't exists": verify_user_doesnt_exist_mock, + "user exists": verify_user_exists_mock, + "the specified user doesn't exist": verify_mock_post_request_to_create_user, + "user is present in DB": verify_mock_delete_request_to_delete_user, + } + + if action == "setup": + setup_mapping[state]() + elif action == "teardown": + teardown_mapping[state]() + return {"message": f"State '{state}' has been {action}"} + + +def run_server() -> None: + """ + Run the flask server. + + This function is required to run the Flask server in a separate process. A + lambda cannot be used as the target of a `multiprocessing.Process` as it + cannot be pickled. + """ + app.run(host=PROVIDER_URL.host, port=PROVIDER_URL.port) + + +def test_provider() -> None: + """ + Test the provider to ensure compliance. + """ + proc = Process(target=run_server, daemon=True) + proc.start() + time.sleep(2) + verifier = Verifier().set_info("v3_http_provider", url=PROVIDER_URL) + verifier.add_source("examples/pacts") + verifier.set_state( + PROVIDER_URL / "_pact" / "callback", + teardown=True, + ) + verifier.verify() + + proc.terminate() + + +def mock_user_doesnt_exist() -> None: + """ + Mock the database for the user doesn't exist state. + """ + import examples.src.flask + + mock_db = MagicMock() + mock_db.get.return_value = None + examples.src.flask.FAKE_DB = mock_db + + +def mock_user_exists() -> None: + """ + Mock the database for the user exists state. + """ + import examples.src.flask + + mock_db = MagicMock() + mock_db.get.return_value = { + "id": 123, + "name": "Verna Hampton", + "created_on": "2024-08-29T04:53:07.337793+00:00", + "ip_address": "10.1.2.3", + "hobbies": ["hiking", "swimming"], + "admin": False, + } + examples.src.flask.FAKE_DB = mock_db + + +def mock_post_request_to_create_user() -> None: + """ + Mock the database for the post request to create a user. + """ + import examples.src.flask + + mock_db = MagicMock() + mock_db.__len__.return_value = 124 + mock_db.__setitem__.return_value = None + mock_db.__getitem__.return_value = { + "id": 124, + "created_on": "2024-09-06T05:07:06.745719+00:00", + "email": "jane@example.com", + "name": "Jane Doe", + } + examples.src.flask.FAKE_DB = mock_db + + +def mock_delete_request_to_delete_user() -> None: + """ + Mock the database for the delete request to delete a user. + """ + import examples.src.flask + + db_values = { + 123: { + "id": 123, + "name": "Verna Hampton", + "created_on": "2024-08-29T04:53:07.337793+00:00", + "ip_address": "10.1.2.3", + "hobbies": ["hiking", "swimming"], + "admin": False, + }, + 124: { + "id": 124, + "name": "Jane Doe", + "created_on": "2024-08-29T04:53:07.337793+00:00", + "ip_address": "10.1.2.5", + "hobbies": ["running", "dancing"], + "admin": False, + }, + } + + mock_db = MagicMock() + mock_db.__delitem__.side_effect = lambda key: db_values.__delitem__(key) + mock_db.__getitem__.side_effect = lambda key: db_values[key] + mock_db.__contains__.side_effect = lambda key: key in db_values + examples.src.flask.FAKE_DB = mock_db + + +def verify_user_doesnt_exist_mock() -> None: + """ + Verify the mock calls for the 'user doesn't exist' state. + + This function checks that the mock for `FAKE_DB.get` was called, + verifies that it returned `None`, + and ensures that it was called with an integer argument. + It then resets the mock for future tests. + + Returns: + str: A message indicating that the 'user doesn't exist' mock has been verified. + """ + import examples.src.flask + + if TYPE_CHECKING: + examples.src.flask.FAKE_DB = MagicMock() + + examples.src.flask.FAKE_DB.get.assert_called() + + assert ( + examples.src.flask.FAKE_DB.get.return_value is None + ), "Expected get() to return None" + + args, _ = examples.src.flask.FAKE_DB.get.call_args + assert isinstance( + args[0], int + ), f"Expected get() to be called with an integer, but got {type(args[0])}" + + examples.src.flask.FAKE_DB.reset_mock() + + +def verify_user_exists_mock() -> None: + """ + Verify the mock calls for the 'user exists' state. + + This function checks that the mock for `FAKE_DB.get` was called, + verifies that it returned the expected user data, + and ensures that it was called with the integer argument `123`. + It then resets the mock for future tests. + + Returns: + str: A message indicating that the 'user exists' mock has been verified. + """ + import examples.src.flask + + if TYPE_CHECKING: + examples.src.flask.FAKE_DB = MagicMock() + + examples.src.flask.FAKE_DB.get.assert_called() + + expected_return = { + "id": 123, + "name": "Verna Hampton", + "created_on": "2024-08-29T04:53:07.337793+00:00", + "ip_address": "10.1.2.3", + "hobbies": ["hiking", "swimming"], + "admin": False, + } + + assert ( + examples.src.flask.FAKE_DB.get.return_value == expected_return + ), "Unexpected return value from get()" + + args, _ = examples.src.flask.FAKE_DB.get.call_args + assert isinstance(args[0], int), ( + "Expected get() to be called with an integer, " + f"but got {type(args[0])}, {args[0]}" + ) + assert args[0] == 123, f"Expected get(123), but got get({args[0]})" + + examples.src.flask.FAKE_DB.reset_mock() + + +def verify_mock_post_request_to_create_user() -> None: + import examples.src.flask + + if TYPE_CHECKING: + examples.src.flask.FAKE_DB = MagicMock() + + examples.src.flask.FAKE_DB.__getitem__.assert_called() + + expected_return = { + "id": 124, + "created_on": "2024-09-06T05:07:06.745719+00:00", + "email": "jane@example.com", + "name": "Jane Doe", + } + + examples.src.flask.FAKE_DB.__len__.assert_called() + logger.info(":::::::%s::::::", expected_return["created_on"]) + assert ( + examples.src.flask.FAKE_DB.__getitem__.return_value == expected_return + ), "Unexpected return value from __getitem__()" + + args, _ = examples.src.flask.FAKE_DB.__getitem__.call_args + assert isinstance( + args[0], int + ), f"Expected get() to be called with an integer, but got {type(args[0])}" + assert args[0] == 124, f"Expected get(124), but got get({args[0]})" + + examples.src.flask.FAKE_DB.reset_mock() + + +def verify_mock_delete_request_to_delete_user() -> None: + import examples.src.flask + + if TYPE_CHECKING: + examples.src.flask.FAKE_DB = MagicMock() + + examples.src.flask.FAKE_DB.__delitem__.assert_called() + + args, _ = examples.src.flask.FAKE_DB.__delitem__.call_args + assert isinstance( + args[0], int + ), f"Expected __delitem__() to be called with an integer, but got {type(args[0])}" + + examples.src.flask.FAKE_DB.reset_mock()