diff --git a/.github/workflows/ci-pact-master.yml b/.github/workflows/ci-pact-master.yml index 1c3e3dfb62ec..a885fe512384 100644 --- a/.github/workflows/ci-pact-master.yml +++ b/.github/workflows/ci-pact-master.yml @@ -37,4 +37,5 @@ jobs: make devenv source .venv/bin/activate cd services/api-server + make install-ci make test-pacts diff --git a/services/api-server/tests/unit/pact_broker/conftest.py b/services/api-server/tests/unit/pact_broker/conftest.py index a5e722fadc40..e63b68a20129 100644 --- a/services/api-server/tests/unit/pact_broker/conftest.py +++ b/services/api-server/tests/unit/pact_broker/conftest.py @@ -76,7 +76,7 @@ def mock_get_current_identity() -> Identity: @pytest.fixture() -def run_test_server( +def running_test_server_url( app: FastAPI, ): """ diff --git a/services/api-server/tests/unit/pact_broker/pacts/01_checkout_release.json b/services/api-server/tests/unit/pact_broker/pacts/01_checkout_release.json new file mode 100644 index 000000000000..9fd2d5b7d12b --- /dev/null +++ b/services/api-server/tests/unit/pact_broker/pacts/01_checkout_release.json @@ -0,0 +1,81 @@ +{ + "consumer": { + "name": "Sim4Life" + }, + "provider": { + "name": "OsparcApiServerCheckoutRelease" + }, + "interactions": [ + { + "description": "Checkout one license", + "request": { + "method": "POST", + "path": "/v0/wallets/35/licensed-items/99580844-77fa-41bb-ad70-02dfaf1e3965/checkout", + "headers": { + "Accept": "application/json", + "Content-Type": "application/json" + }, + "body": { + "number_of_seats": 1, + "service_run_id": "1740149365_21a9352a-1d46-41f9-9a9b-42ac888f5afb" + } + }, + "response": { + "status": 200, + "headers": { + "Content-Length": "294", + "Content-Type": "application/json", + "Server": "uvicorn" + }, + "body": { + "key": "MODEL_IX_HEAD", + "licensed_item_checkout_id": "25262183-392c-4268-9311-3c4256c46012", + "licensed_item_id": "99580844-77fa-41bb-ad70-02dfaf1e3965", + "num_of_seats": 1, + "product_name": "s4l", + "started_at": "2025-02-21T15:04:47.673828Z", + "stopped_at": null, + "user_id": 425, + "version": "1.0.0", + "wallet_id": 35 + } + } + }, + { + "description": "Release item", + "request": { + "method": "POST", + "path": "/v0/licensed-items/99580844-77fa-41bb-ad70-02dfaf1e3965/checked-out-items/25262183-392c-4268-9311-3c4256c46012/release", + "headers": { + "Accept": "application/json", + "Content-Type": "application/json" + } + }, + "response": { + "status": 200, + "headers": { + "Content-Length": "319", + "Content-Type": "application/json", + "Server": "uvicorn" + }, + "body": { + "key": "MODEL_IX_HEAD", + "licensed_item_checkout_id": "25262183-392c-4268-9311-3c4256c46012", + "licensed_item_id": "99580844-77fa-41bb-ad70-02dfaf1e3965", + "num_of_seats": 1, + "product_name": "s4l", + "started_at": "2025-02-21T15:04:47.673828Z", + "stopped_at": "2025-02-21T15:04:47.901169Z", + "user_id": 425, + "version": "1.0.0", + "wallet_id": 35 + } + } + } + ], + "metadata": { + "pactSpecification": { + "version": "3.0.0" + } + } +} diff --git a/services/api-server/tests/unit/pact_broker/pacts/05_licensed_items.json b/services/api-server/tests/unit/pact_broker/pacts/05_licensed_items.json index 2de4e26121c8..977823e6efbe 100644 --- a/services/api-server/tests/unit/pact_broker/pacts/05_licensed_items.json +++ b/services/api-server/tests/unit/pact_broker/pacts/05_licensed_items.json @@ -1,9 +1,9 @@ { "consumer": { - "name": "XOsparcApiClient" + "name": "Sim4Life" }, "provider": { - "name": "OsparcApiProvider" + "name": "OsparcApiServerLicensedItems" }, "interactions": [ { diff --git a/services/api-server/tests/unit/pact_broker/test_pact_checkout_release.py b/services/api-server/tests/unit/pact_broker/test_pact_checkout_release.py new file mode 100644 index 000000000000..62ff031ad788 --- /dev/null +++ b/services/api-server/tests/unit/pact_broker/test_pact_checkout_release.py @@ -0,0 +1,132 @@ +# pylint: disable=redefined-outer-name +# pylint: disable=unused-argument +# pylint: disable=unused-variable +# pylint: disable=too-many-arguments + + +import os + +import pytest +from fastapi import FastAPI +from models_library.api_schemas_webserver.licensed_items_checkouts import ( + LicensedItemCheckoutRpcGet, +) +from pact.v3 import Verifier +from pytest_mock import MockerFixture +from simcore_service_api_server._meta import API_VERSION +from simcore_service_api_server.api.dependencies.resource_usage_tracker_rpc import ( + get_resource_usage_tracker_client, +) +from simcore_service_api_server.api.dependencies.webserver_rpc import ( + get_wb_api_rpc_client, +) +from simcore_service_api_server.services_rpc.resource_usage_tracker import ( + ResourceUsageTrackerClient, +) +from simcore_service_api_server.services_rpc.wb_api_server import WbApiRpcClient + +# Fake response based on values from 01_checkout_release.json +EXPECTED_CHECKOUT = LicensedItemCheckoutRpcGet.model_validate( + { + "key": "MODEL_IX_HEAD", + "licensed_item_checkout_id": "25262183-392c-4268-9311-3c4256c46012", + "licensed_item_id": "99580844-77fa-41bb-ad70-02dfaf1e3965", + "num_of_seats": 1, + "product_name": "s4l", + "started_at": "2025-02-21T15:04:47.673828Z", + "stopped_at": None, + "user_id": 425, + "version": "1.0.0", + "wallet_id": 35, + } +) +assert EXPECTED_CHECKOUT.stopped_at is None + + +EXPECTED_RELEASE = LicensedItemCheckoutRpcGet.model_validate( + { + "key": "MODEL_IX_HEAD", + "licensed_item_checkout_id": "25262183-392c-4268-9311-3c4256c46012", + "licensed_item_id": "99580844-77fa-41bb-ad70-02dfaf1e3965", + "num_of_seats": 1, + "product_name": "s4l", + "started_at": "2025-02-21T15:04:47.673828Z", + "stopped_at": "2025-02-21T15:04:47.901169Z", + "user_id": 425, + "version": "1.0.0", + "wallet_id": 35, + } +) +assert EXPECTED_RELEASE.stopped_at is not None + + +class DummyRpcClient: + pass + + +@pytest.fixture +async def mock_wb_api_server_rpc(app: FastAPI, mocker: MockerFixture) -> None: + + app.dependency_overrides[get_wb_api_rpc_client] = lambda: WbApiRpcClient( + _client=DummyRpcClient() + ) + + mocker.patch( + "simcore_service_api_server.services_rpc.wb_api_server._checkout_licensed_item_for_wallet", + return_value=EXPECTED_CHECKOUT, + ) + + mocker.patch( + "simcore_service_api_server.services_rpc.wb_api_server._release_licensed_item_for_wallet", + return_value=EXPECTED_RELEASE, + ) + + +@pytest.fixture +async def mock_rut_server_rpc(app: FastAPI, mocker: MockerFixture) -> None: + + app.dependency_overrides[get_resource_usage_tracker_client] = ( + lambda: ResourceUsageTrackerClient(_client=DummyRpcClient()) + ) + + mocker.patch( + "simcore_service_api_server.services_rpc.resource_usage_tracker._get_licensed_item_checkout", + return_value=EXPECTED_CHECKOUT, + ) + + +@pytest.mark.skipif( + not os.getenv("PACT_BROKER_URL"), + reason="This test runs only if PACT_BROKER_URL is provided", +) +def test_provider_against_pact( + pact_broker_credentials: tuple[str, str, str], + mock_wb_api_server_rpc: None, + mock_rut_server_rpc: None, + running_test_server_url: str, +) -> None: + """ + Use the Pact Verifier to check the real provider + against the generated contract. + """ + broker_url, broker_username, broker_password = pact_broker_credentials + + broker_builder = ( + Verifier("OsparcApiServerCheckoutRelease") + .add_transport(url=running_test_server_url) + .broker_source( + broker_url, + username=broker_username, + password=broker_password, + selector=True, + ) + ) + + # NOTE: If you want to filter/test against specific contract use tags + verifier = broker_builder.consumer_tags( + "checkout_release" # <-- Here you define which pact to verify + ).build() + + # Set API version and run verification + verifier.set_publish_options(version=API_VERSION, tags=None, branch=None) + verifier.verify() diff --git a/services/api-server/tests/unit/pact_broker/test_pact_licensed_items.py b/services/api-server/tests/unit/pact_broker/test_pact_licensed_items.py index 697c448cfcbd..f04e4bd47378 100644 --- a/services/api-server/tests/unit/pact_broker/test_pact_licensed_items.py +++ b/services/api-server/tests/unit/pact_broker/test_pact_licensed_items.py @@ -137,7 +137,7 @@ class DummyRpcClient: @pytest.fixture -async def mock_wb_api_server_rpc(app: FastAPI, mocker: MockerFixture) -> MockerFixture: +async def mock_wb_api_server_rpc(app: FastAPI, mocker: MockerFixture) -> None: app.dependency_overrides[get_wb_api_rpc_client] = lambda: WbApiRpcClient( _client=DummyRpcClient() @@ -148,8 +148,6 @@ async def mock_wb_api_server_rpc(app: FastAPI, mocker: MockerFixture) -> MockerF return_value=EXPECTED_LICENSED_ITEMS_PAGE, ) - return mocker - @pytest.mark.skipif( not os.getenv("PACT_BROKER_URL"), @@ -157,8 +155,8 @@ async def mock_wb_api_server_rpc(app: FastAPI, mocker: MockerFixture) -> MockerF ) def test_provider_against_pact( pact_broker_credentials: tuple[str, str, str], - mock_wb_api_server_rpc: MockerFixture, - run_test_server: str, + mock_wb_api_server_rpc: None, + running_test_server_url: str, ) -> None: """ Use the Pact Verifier to check the real provider @@ -167,8 +165,8 @@ def test_provider_against_pact( broker_url, broker_username, broker_password = pact_broker_credentials broker_builder = ( - Verifier("OsparcApiProvider") - .add_transport(url=run_test_server) + Verifier("OsparcApiServerLicensedItems") + .add_transport(url=running_test_server_url) .broker_source( broker_url, username=broker_username,