Skip to content

Commit f7d2ebf

Browse files
🎨 PACT - add checkout release contract test (ITISFoundation#7303)
1 parent a776cef commit f7d2ebf

File tree

6 files changed

+222
-10
lines changed

6 files changed

+222
-10
lines changed

.github/workflows/ci-pact-master.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,5 @@ jobs:
3737
make devenv
3838
source .venv/bin/activate
3939
cd services/api-server
40+
make install-ci
4041
make test-pacts

services/api-server/tests/unit/pact_broker/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def mock_get_current_identity() -> Identity:
7676

7777

7878
@pytest.fixture()
79-
def run_test_server(
79+
def running_test_server_url(
8080
app: FastAPI,
8181
):
8282
"""
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{
2+
"consumer": {
3+
"name": "Sim4Life"
4+
},
5+
"provider": {
6+
"name": "OsparcApiServerCheckoutRelease"
7+
},
8+
"interactions": [
9+
{
10+
"description": "Checkout one license",
11+
"request": {
12+
"method": "POST",
13+
"path": "/v0/wallets/35/licensed-items/99580844-77fa-41bb-ad70-02dfaf1e3965/checkout",
14+
"headers": {
15+
"Accept": "application/json",
16+
"Content-Type": "application/json"
17+
},
18+
"body": {
19+
"number_of_seats": 1,
20+
"service_run_id": "1740149365_21a9352a-1d46-41f9-9a9b-42ac888f5afb"
21+
}
22+
},
23+
"response": {
24+
"status": 200,
25+
"headers": {
26+
"Content-Length": "294",
27+
"Content-Type": "application/json",
28+
"Server": "uvicorn"
29+
},
30+
"body": {
31+
"key": "MODEL_IX_HEAD",
32+
"licensed_item_checkout_id": "25262183-392c-4268-9311-3c4256c46012",
33+
"licensed_item_id": "99580844-77fa-41bb-ad70-02dfaf1e3965",
34+
"num_of_seats": 1,
35+
"product_name": "s4l",
36+
"started_at": "2025-02-21T15:04:47.673828Z",
37+
"stopped_at": null,
38+
"user_id": 425,
39+
"version": "1.0.0",
40+
"wallet_id": 35
41+
}
42+
}
43+
},
44+
{
45+
"description": "Release item",
46+
"request": {
47+
"method": "POST",
48+
"path": "/v0/licensed-items/99580844-77fa-41bb-ad70-02dfaf1e3965/checked-out-items/25262183-392c-4268-9311-3c4256c46012/release",
49+
"headers": {
50+
"Accept": "application/json",
51+
"Content-Type": "application/json"
52+
}
53+
},
54+
"response": {
55+
"status": 200,
56+
"headers": {
57+
"Content-Length": "319",
58+
"Content-Type": "application/json",
59+
"Server": "uvicorn"
60+
},
61+
"body": {
62+
"key": "MODEL_IX_HEAD",
63+
"licensed_item_checkout_id": "25262183-392c-4268-9311-3c4256c46012",
64+
"licensed_item_id": "99580844-77fa-41bb-ad70-02dfaf1e3965",
65+
"num_of_seats": 1,
66+
"product_name": "s4l",
67+
"started_at": "2025-02-21T15:04:47.673828Z",
68+
"stopped_at": "2025-02-21T15:04:47.901169Z",
69+
"user_id": 425,
70+
"version": "1.0.0",
71+
"wallet_id": 35
72+
}
73+
}
74+
}
75+
],
76+
"metadata": {
77+
"pactSpecification": {
78+
"version": "3.0.0"
79+
}
80+
}
81+
}

services/api-server/tests/unit/pact_broker/pacts/05_licensed_items.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"consumer": {
3-
"name": "XOsparcApiClient"
3+
"name": "Sim4Life"
44
},
55
"provider": {
6-
"name": "OsparcApiProvider"
6+
"name": "OsparcApiServerLicensedItems"
77
},
88
"interactions": [
99
{
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# pylint: disable=redefined-outer-name
2+
# pylint: disable=unused-argument
3+
# pylint: disable=unused-variable
4+
# pylint: disable=too-many-arguments
5+
6+
7+
import os
8+
9+
import pytest
10+
from fastapi import FastAPI
11+
from models_library.api_schemas_webserver.licensed_items_checkouts import (
12+
LicensedItemCheckoutRpcGet,
13+
)
14+
from pact.v3 import Verifier
15+
from pytest_mock import MockerFixture
16+
from simcore_service_api_server._meta import API_VERSION
17+
from simcore_service_api_server.api.dependencies.resource_usage_tracker_rpc import (
18+
get_resource_usage_tracker_client,
19+
)
20+
from simcore_service_api_server.api.dependencies.webserver_rpc import (
21+
get_wb_api_rpc_client,
22+
)
23+
from simcore_service_api_server.services_rpc.resource_usage_tracker import (
24+
ResourceUsageTrackerClient,
25+
)
26+
from simcore_service_api_server.services_rpc.wb_api_server import WbApiRpcClient
27+
28+
# Fake response based on values from 01_checkout_release.json
29+
EXPECTED_CHECKOUT = LicensedItemCheckoutRpcGet.model_validate(
30+
{
31+
"key": "MODEL_IX_HEAD",
32+
"licensed_item_checkout_id": "25262183-392c-4268-9311-3c4256c46012",
33+
"licensed_item_id": "99580844-77fa-41bb-ad70-02dfaf1e3965",
34+
"num_of_seats": 1,
35+
"product_name": "s4l",
36+
"started_at": "2025-02-21T15:04:47.673828Z",
37+
"stopped_at": None,
38+
"user_id": 425,
39+
"version": "1.0.0",
40+
"wallet_id": 35,
41+
}
42+
)
43+
assert EXPECTED_CHECKOUT.stopped_at is None
44+
45+
46+
EXPECTED_RELEASE = LicensedItemCheckoutRpcGet.model_validate(
47+
{
48+
"key": "MODEL_IX_HEAD",
49+
"licensed_item_checkout_id": "25262183-392c-4268-9311-3c4256c46012",
50+
"licensed_item_id": "99580844-77fa-41bb-ad70-02dfaf1e3965",
51+
"num_of_seats": 1,
52+
"product_name": "s4l",
53+
"started_at": "2025-02-21T15:04:47.673828Z",
54+
"stopped_at": "2025-02-21T15:04:47.901169Z",
55+
"user_id": 425,
56+
"version": "1.0.0",
57+
"wallet_id": 35,
58+
}
59+
)
60+
assert EXPECTED_RELEASE.stopped_at is not None
61+
62+
63+
class DummyRpcClient:
64+
pass
65+
66+
67+
@pytest.fixture
68+
async def mock_wb_api_server_rpc(app: FastAPI, mocker: MockerFixture) -> None:
69+
70+
app.dependency_overrides[get_wb_api_rpc_client] = lambda: WbApiRpcClient(
71+
_client=DummyRpcClient()
72+
)
73+
74+
mocker.patch(
75+
"simcore_service_api_server.services_rpc.wb_api_server._checkout_licensed_item_for_wallet",
76+
return_value=EXPECTED_CHECKOUT,
77+
)
78+
79+
mocker.patch(
80+
"simcore_service_api_server.services_rpc.wb_api_server._release_licensed_item_for_wallet",
81+
return_value=EXPECTED_RELEASE,
82+
)
83+
84+
85+
@pytest.fixture
86+
async def mock_rut_server_rpc(app: FastAPI, mocker: MockerFixture) -> None:
87+
88+
app.dependency_overrides[get_resource_usage_tracker_client] = (
89+
lambda: ResourceUsageTrackerClient(_client=DummyRpcClient())
90+
)
91+
92+
mocker.patch(
93+
"simcore_service_api_server.services_rpc.resource_usage_tracker._get_licensed_item_checkout",
94+
return_value=EXPECTED_CHECKOUT,
95+
)
96+
97+
98+
@pytest.mark.skipif(
99+
not os.getenv("PACT_BROKER_URL"),
100+
reason="This test runs only if PACT_BROKER_URL is provided",
101+
)
102+
def test_provider_against_pact(
103+
pact_broker_credentials: tuple[str, str, str],
104+
mock_wb_api_server_rpc: None,
105+
mock_rut_server_rpc: None,
106+
running_test_server_url: str,
107+
) -> None:
108+
"""
109+
Use the Pact Verifier to check the real provider
110+
against the generated contract.
111+
"""
112+
broker_url, broker_username, broker_password = pact_broker_credentials
113+
114+
broker_builder = (
115+
Verifier("OsparcApiServerCheckoutRelease")
116+
.add_transport(url=running_test_server_url)
117+
.broker_source(
118+
broker_url,
119+
username=broker_username,
120+
password=broker_password,
121+
selector=True,
122+
)
123+
)
124+
125+
# NOTE: If you want to filter/test against specific contract use tags
126+
verifier = broker_builder.consumer_tags(
127+
"checkout_release" # <-- Here you define which pact to verify
128+
).build()
129+
130+
# Set API version and run verification
131+
verifier.set_publish_options(version=API_VERSION, tags=None, branch=None)
132+
verifier.verify()

services/api-server/tests/unit/pact_broker/test_pact_licensed_items.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ class DummyRpcClient:
137137

138138

139139
@pytest.fixture
140-
async def mock_wb_api_server_rpc(app: FastAPI, mocker: MockerFixture) -> MockerFixture:
140+
async def mock_wb_api_server_rpc(app: FastAPI, mocker: MockerFixture) -> None:
141141

142142
app.dependency_overrides[get_wb_api_rpc_client] = lambda: WbApiRpcClient(
143143
_client=DummyRpcClient()
@@ -148,17 +148,15 @@ async def mock_wb_api_server_rpc(app: FastAPI, mocker: MockerFixture) -> MockerF
148148
return_value=EXPECTED_LICENSED_ITEMS_PAGE,
149149
)
150150

151-
return mocker
152-
153151

154152
@pytest.mark.skipif(
155153
not os.getenv("PACT_BROKER_URL"),
156154
reason="This test runs only if PACT_BROKER_URL is provided",
157155
)
158156
def test_provider_against_pact(
159157
pact_broker_credentials: tuple[str, str, str],
160-
mock_wb_api_server_rpc: MockerFixture,
161-
run_test_server: str,
158+
mock_wb_api_server_rpc: None,
159+
running_test_server_url: str,
162160
) -> None:
163161
"""
164162
Use the Pact Verifier to check the real provider
@@ -167,8 +165,8 @@ def test_provider_against_pact(
167165
broker_url, broker_username, broker_password = pact_broker_credentials
168166

169167
broker_builder = (
170-
Verifier("OsparcApiProvider")
171-
.add_transport(url=run_test_server)
168+
Verifier("OsparcApiServerLicensedItems")
169+
.add_transport(url=running_test_server_url)
172170
.broker_source(
173171
broker_url,
174172
username=broker_username,

0 commit comments

Comments
 (0)