Skip to content

Commit df04062

Browse files
authored
[MPT-14908] Added seeding and e2e tests for commerce subscriptions (#161)
Added seeding and e2e tests for commerce subscriptions https://softwareone.atlassian.net/browse/MPT-14908 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Subscription termination and update operations available in the client (terminate now accepts optional payload). * **Improvements** * Lint/test config updated to mark flaky tests and suppress specific style checks. * **Tests** * New and expanded unit and end-to-end tests covering subscription lifecycle (create, retrieve, list, update, terminate, render); new e2e config keys and test fixtures added. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2 parents f689af5 + d0f78d4 commit df04062

File tree

10 files changed

+365
-32
lines changed

10 files changed

+365
-32
lines changed

e2e_config.test.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,8 @@
3434
"commerce.product.listing.id": "LST-5489-0806",
3535
"commerce.product.template.id": "TPL-1767-7355-0002",
3636
"commerce.user.id": "USR-4303-2348",
37+
"commerce.subscription.agreement.id": "AGR-2473-3299-1721",
38+
"commerce.subscription.id": "SUB-3678-1831-2188",
39+
"commerce.subscription.product.item.id": "ITM-1767-7355-0001",
3740
"notifications.message.id": "MSG-0000-6215-1019-0139"
3841
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from mpt_api_client.models.model import ResourceData
2+
3+
4+
class TerminateMixin[Model]:
5+
"""Terminate resource mixin."""
6+
7+
def terminate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model:
8+
"""Terminate resource.
9+
10+
Args:
11+
resource_id: Resource ID
12+
resource_data: Resource data
13+
14+
Returns:
15+
Terminated resource.
16+
"""
17+
return self._resource_action(resource_id, "POST", "terminate", json=resource_data) # type: ignore[attr-defined, no-any-return]
18+
19+
20+
class AsyncTerminateMixin[Model]:
21+
"""Asynchronous terminate resource mixin."""
22+
23+
async def terminate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model:
24+
"""Terminate resource.
25+
26+
Args:
27+
resource_id: Resource ID
28+
resource_data: Resource data
29+
30+
Returns:
31+
Terminated resource.
32+
"""
33+
return await self._resource_action(resource_id, "POST", "terminate", json=resource_data) # type: ignore[attr-defined, no-any-return]

mpt_api_client/resources/commerce/subscriptions.py

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
from mpt_api_client.http.mixins import (
66
AsyncCollectionMixin,
77
AsyncCreateMixin,
8-
AsyncDeleteMixin,
98
AsyncGetMixin,
9+
AsyncUpdateMixin,
1010
CollectionMixin,
1111
CreateMixin,
12-
DeleteMixin,
1312
GetMixin,
13+
UpdateMixin,
1414
)
15-
from mpt_api_client.models import Model, ResourceData
15+
from mpt_api_client.models import Model
16+
from mpt_api_client.resources.commerce.mixins import AsyncTerminateMixin, TerminateMixin
1617

1718

1819
class Subscription(Model):
@@ -29,9 +30,10 @@ class SubscriptionsServiceConfig:
2930

3031
class SubscriptionsService( # noqa: WPS215
3132
CreateMixin[Subscription],
32-
DeleteMixin,
33+
UpdateMixin[Subscription],
3334
GetMixin[Subscription],
3435
CollectionMixin[Subscription],
36+
TerminateMixin[Subscription],
3537
Service[Subscription],
3638
SubscriptionsServiceConfig,
3739
):
@@ -49,24 +51,13 @@ def render(self, resource_id: str) -> str:
4951
response = self._resource_do_request(resource_id, "GET", "render")
5052
return response.text
5153

52-
def terminate(self, resource_id: str, resource_data: ResourceData) -> Subscription:
53-
"""Terminate subscription.
54-
55-
Args:
56-
resource_id: Order resource ID
57-
resource_data: Order resource data
58-
59-
Returns:
60-
Subscription template text in markdown format.
61-
"""
62-
return self._resource_action(resource_id, "POST", "terminate", json=resource_data)
63-
6454

6555
class AsyncSubscriptionsService( # noqa: WPS215
6656
AsyncCreateMixin[Subscription],
67-
AsyncDeleteMixin,
57+
AsyncUpdateMixin[Subscription],
6858
AsyncGetMixin[Subscription],
6959
AsyncCollectionMixin[Subscription],
60+
AsyncTerminateMixin[Subscription],
7061
AsyncService[Subscription],
7162
SubscriptionsServiceConfig,
7263
):
@@ -83,15 +74,3 @@ async def render(self, resource_id: str) -> str:
8374
"""
8475
response = await self._resource_do_request(resource_id, "GET", "render")
8576
return response.text
86-
87-
async def terminate(self, resource_id: str, resource_data: ResourceData) -> Subscription:
88-
"""Terminate subscription.
89-
90-
Args:
91-
resource_id: Order resource ID
92-
resource_data: Order resource data
93-
94-
Returns:
95-
Subscription template text in markdown format.
96-
"""
97-
return await self._resource_action(resource_id, "POST", "terminate", json=resource_data)

pyproject.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ filterwarnings = [
7474
"ignore:pkg_resources is deprecated as an API:DeprecationWarning",
7575
]
7676
rp_project = "mpt-api-python-client"
77+
markers = [
78+
"flaky: mark test as flaky (may fail intermittently)",
79+
]
7780

7881
[tool.coverage.run]
7982
branch = true
@@ -120,14 +123,16 @@ per-file-ignores = [
120123
"mpt_api_client/resources/catalog/*.py: WPS110 WPS214 WPS215 WPS235",
121124
"mpt_api_client/resources/catalog/mixins.py: WPS110 WPS202 WPS214 WPS215 WPS235",
122125
"mpt_api_client/resources/catalog/products.py: WPS204 WPS214 WPS215 WPS235",
126+
"mpt_api_client/resources/commerce/*.py: WPS235 WPS215",
123127
"mpt_api_client/rql/query_builder.py: WPS110 WPS115 WPS210 WPS214",
124128
"tests/e2e/accounts/*.py: WPS430 WPS202",
125129
"tests/e2e/catalog/*.py: WPS202 WPS421",
126130
"tests/e2e/catalog/items/*.py: WPS110 WPS202",
127-
"tests/e2e/commerce/*.py: WPS204 WPS453",
131+
"tests/e2e/commerce/*.py: WPS202 WPS204 WPS453",
128132
"tests/e2e/commerce/agreement/*.py: WPS202",
129133
"tests/e2e/commerce/agreement/attachment/*.py: WPS202",
130134
"tests/e2e/commerce/order/*.py: WPS202 WPS204",
135+
"tests/e2e/commerce/subscription/*.py: WPS202",
131136
"tests/unit/http/test_async_service.py: WPS204 WPS202",
132137
"tests/unit/http/test_service.py: WPS204 WPS202",
133138
"tests/unit/http/test_mixins.py: WPS204 WPS202 WPS210",

tests/e2e/commerce/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,13 @@ def commerce_product_template_id(e2e_config):
2929
@pytest.fixture
3030
def commerce_user_id(e2e_config):
3131
return e2e_config["commerce.user.id"]
32+
33+
34+
@pytest.fixture
35+
def subscription_item_id(e2e_config):
36+
return e2e_config["commerce.subscription.product.item.id"]
37+
38+
39+
@pytest.fixture
40+
def subscription_agreement_id(e2e_config):
41+
return e2e_config["commerce.subscription.agreement.id"]
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import pytest
2+
from freezegun import freeze_time
3+
4+
5+
@pytest.fixture
6+
def subscription_id(e2e_config):
7+
return e2e_config["commerce.subscription.id"]
8+
9+
10+
@pytest.fixture
11+
def invalid_subscription_id():
12+
return "SUB-0000-0000-0000"
13+
14+
15+
@pytest.fixture
16+
def subscription_factory(subscription_agreement_id, subscription_item_id):
17+
@freeze_time("2025-11-14T09:00:00.000Z")
18+
def factory(
19+
name: str = "E2E Created Subscription",
20+
external_vendor_id: str = "ext-vendor-id",
21+
quantity: int = 1,
22+
):
23+
return {
24+
"name": name,
25+
"startDate": "2025-11-03T09:00:00.000Z",
26+
"commitmentDate": "2026-11-02T09:00:00.000Z",
27+
"autoRenew": True,
28+
"agreement": {"id": subscription_agreement_id},
29+
"externalIds": {"vendor": external_vendor_id},
30+
"template": None,
31+
"lines": [
32+
{
33+
"item": {"id": subscription_item_id},
34+
"quantity": quantity,
35+
"price": {"unitPP": 10},
36+
}
37+
],
38+
"parameters": {"fulfillment": []},
39+
}
40+
41+
return factory
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import pytest
2+
3+
from mpt_api_client.exceptions import MPTAPIError
4+
from mpt_api_client.rql.query_builder import RQLQuery
5+
6+
pytestmark = [pytest.mark.flaky]
7+
8+
9+
@pytest.fixture
10+
async def created_subscription(async_mpt_vendor, subscription_factory):
11+
subscription_data = subscription_factory()
12+
13+
subscription = await async_mpt_vendor.commerce.subscriptions.create(subscription_data)
14+
15+
yield subscription
16+
17+
try:
18+
await async_mpt_vendor.commerce.subscriptions.terminate(subscription.id)
19+
except MPTAPIError as error:
20+
print(f"TEARDOWN - Unable to terminate subscription: {getattr(error, 'title', str(error))}") # noqa: WPS421
21+
22+
23+
async def test_get_subscription_by_id(async_mpt_vendor, subscription_id):
24+
result = await async_mpt_vendor.commerce.subscriptions.get(subscription_id)
25+
26+
assert result is not None
27+
28+
29+
async def test_list_subscriptions(async_mpt_vendor):
30+
limit = 10
31+
32+
result = await async_mpt_vendor.commerce.subscriptions.fetch_page(limit=limit)
33+
34+
assert result is not None
35+
36+
37+
async def test_get_subscription_by_id_not_found(async_mpt_vendor, invalid_subscription_id):
38+
with pytest.raises(MPTAPIError, match="404 Not Found"):
39+
await async_mpt_vendor.commerce.subscriptions.get(invalid_subscription_id)
40+
41+
42+
async def test_filter_subscriptions(async_mpt_vendor, subscription_id):
43+
select_fields = ["-externalIds"]
44+
filtered_subscriptions = (
45+
async_mpt_vendor.commerce.subscriptions.filter(RQLQuery(id=subscription_id))
46+
.filter(RQLQuery(name="E2E Seeded Subscription"))
47+
.select(*select_fields)
48+
)
49+
50+
result = [subscription async for subscription in filtered_subscriptions.iterate()]
51+
52+
assert len(result) == 1
53+
54+
55+
def test_create_subscription(created_subscription):
56+
result = created_subscription
57+
58+
assert result is not None
59+
60+
61+
async def test_update_subscription(async_mpt_vendor, created_subscription):
62+
updated_subscription_data = {
63+
"name": "E2E Updated Subscription",
64+
}
65+
66+
result = await async_mpt_vendor.commerce.subscriptions.update(
67+
created_subscription.id, updated_subscription_data
68+
)
69+
70+
assert result is not None
71+
72+
73+
async def test_terminate_subscription(async_mpt_vendor, created_subscription):
74+
result = await async_mpt_vendor.commerce.subscriptions.terminate(created_subscription.id)
75+
76+
assert result is not None
77+
78+
79+
async def test_render_subscription(async_mpt_vendor, created_subscription):
80+
result = await async_mpt_vendor.commerce.subscriptions.render(created_subscription.id)
81+
82+
assert result is not None
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import pytest
2+
3+
from mpt_api_client.exceptions import MPTAPIError
4+
from mpt_api_client.rql.query_builder import RQLQuery
5+
6+
pytestmark = [pytest.mark.flaky]
7+
8+
9+
@pytest.fixture
10+
def created_subscription(mpt_vendor, subscription_factory):
11+
subscription_data = subscription_factory()
12+
13+
subscription = mpt_vendor.commerce.subscriptions.create(subscription_data)
14+
15+
yield subscription
16+
17+
try:
18+
mpt_vendor.commerce.subscriptions.terminate(subscription.id)
19+
except MPTAPIError as error:
20+
print(f"TEARDOWN - Unable to terminate subscription: {getattr(error, 'title', str(error))}") # noqa: WPS421
21+
22+
23+
def test_get_subscription_by_id(mpt_vendor, subscription_id):
24+
result = mpt_vendor.commerce.subscriptions.get(subscription_id)
25+
26+
assert result is not None
27+
28+
29+
def test_list_subscriptions(mpt_vendor):
30+
limit = 10
31+
32+
result = mpt_vendor.commerce.subscriptions.fetch_page(limit=limit)
33+
34+
assert result is not None
35+
36+
37+
def test_get_subscription_by_id_not_found(mpt_vendor, invalid_subscription_id):
38+
with pytest.raises(MPTAPIError, match="404 Not Found"):
39+
mpt_vendor.commerce.subscriptions.get(invalid_subscription_id)
40+
41+
42+
def test_filter_subscriptions(mpt_vendor, subscription_id):
43+
select_fields = ["-externalIds"]
44+
filtered_subscriptions = (
45+
mpt_vendor.commerce.subscriptions.filter(RQLQuery(id=subscription_id))
46+
.filter(RQLQuery(name="E2E Seeded Subscription"))
47+
.select(*select_fields)
48+
)
49+
50+
result = list(filtered_subscriptions.iterate())
51+
52+
assert len(result) == 1
53+
54+
55+
def test_create_subscription(created_subscription):
56+
result = created_subscription
57+
58+
assert result is not None
59+
60+
61+
def test_update_subscription(mpt_vendor, created_subscription):
62+
updated_subscription_data = {
63+
"name": "E2E Updated Subscription",
64+
}
65+
66+
result = mpt_vendor.commerce.subscriptions.update(
67+
created_subscription.id, updated_subscription_data
68+
)
69+
70+
assert result is not None
71+
72+
73+
def test_terminate_subscription(mpt_vendor, created_subscription):
74+
result = mpt_vendor.commerce.subscriptions.terminate(created_subscription.id)
75+
76+
assert result is not None
77+
78+
79+
def test_render_subscription(mpt_vendor, created_subscription):
80+
result = mpt_vendor.commerce.subscriptions.render(created_subscription.id)
81+
82+
assert result is not None

0 commit comments

Comments
 (0)