Skip to content

Commit f83797a

Browse files
authored
MPT-20329: Added endpoints and e2e tests for program terms (#312)
This pull request introduces a new "Program Terms" resource to the API client, providing both synchronous and asynchronous services for managing program terms. It includes the implementation of the resource, integration into the main program services, end-to-end and unit tests, and updates to configuration and linting settings. The most important changes are: **Program Terms Resource Implementation:** * Added `Term`, `TermService`, and `AsyncTermService` classes in `mpt_api_client/resources/program/programs_terms.py` to support CRUD, publish, and unpublish operations for program terms, including model definition and service configuration. **Integration with Program Services:** * Updated `mpt_api_client/resources/program/programs.py` to import and expose the new `terms` and `async terms` service methods, making program terms accessible via the main program client. [[1]](diffhunk://#diff-05ce5b7611ffd8756a39d2e4c9eacda5896e22ddb7801af98e35ec0cd0e9dc4aR33-R36) [[2]](diffhunk://#diff-05ce5b7611ffd8756a39d2e4c9eacda5896e22ddb7801af98e35ec0cd0e9dc4aR128-R131) [[3]](diffhunk://#diff-05ce5b7611ffd8756a39d2e4c9eacda5896e22ddb7801af98e35ec0cd0e9dc4aR183-R188) * Updated references in `tests/unit/resources/program/test_programs.py` to include tests for the new terms services. [[1]](diffhunk://#diff-57c1fc2a8ee6b316dfadf82a52e52ba29deeaabc135475c62652eb4df81b90aaR27-R30) [[2]](diffhunk://#diff-57c1fc2a8ee6b316dfadf82a52e52ba29deeaabc135475c62652eb4df81b90aaR111) [[3]](diffhunk://#diff-57c1fc2a8ee6b316dfadf82a52e52ba29deeaabc135475c62652eb4df81b90aaR129) **Testing:** * Added comprehensive end-to-end tests for both async and sync program terms operations in `tests/e2e/program/program/term/test_async_term.py` and `test_sync_term.py`, including create, update, get, delete, filter, publish, and unpublish scenarios. [[1]](diffhunk://#diff-20c75d89dd3d262efdd7675362c346e2bf41bd622efe4dc7b57238a2547d1e0aR1-R85) [[2]](diffhunk://#diff-431a1ef4de2193e9e352403b2d279647b967bbda2157d5a48e40b6c42c8f5314R1-R85) * Added unit tests for the new resource in `tests/unit/resources/program/test_programs_terms.py`, covering endpoints, method presence, field mapping, and optional fields. * Added fixtures and configuration for term-related e2e tests in `tests/e2e/program/program/term/conftest.py`. **Configuration and Linting:** * Updated `e2e_config.test.json` to include a term ID for use in tests. * Updated `pyproject.toml` to add linting ignores for the new test files. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> Closes [MPT-20329](https://softwareone.atlassian.net/browse/MPT-20329) - Added new `Term` model with fields for term metadata (name, description, display order, status) and references (program, audit) - Implemented `TermService` and `AsyncTermService` classes supporting CRUD operations, publish/unpublish, filtering, and iteration - Exposed term services through `ProgramsService` and `AsyncProgramsService` via new `terms(program_id)` methods - Added comprehensive end-to-end tests for both synchronous and asynchronous term operations, including create, update, get, delete, filter, publish, and unpublish scenarios - Added unit tests validating endpoints, method presence, field mapping, and optional field handling - Updated e2e configuration with program term ID (`PTC-9643-3741-0001`) - Added linting configuration ignores for new test files in pyproject.toml <!-- end of auto-generated comment: release notes by coderabbit.ai --> [MPT-20329]: https://softwareone.atlassian.net/browse/MPT-20329?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2 parents 10c2ca2 + e18fe84 commit f83797a

9 files changed

Lines changed: 364 additions & 1 deletion

File tree

e2e_config.test.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,6 @@
7575
"program.parameter.group.id": "PPG-9643-3741-0002",
7676
"program.parameter.id": "PPM-9643-3741-0001",
7777
"program.program.id": "PRG-9643-3741",
78-
"program.template.id": "PTM-9643-3741-0004"
78+
"program.template.id": "PTM-9643-3741-0004",
79+
"program.terms.id": "PTC-9643-3741-0001"
7980
}

mpt_api_client/resources/program/programs.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
AsyncTemplatesService,
3131
TemplatesService,
3232
)
33+
from mpt_api_client.resources.program.programs_terms import (
34+
AsyncTermService,
35+
TermService,
36+
)
3337

3438

3539
class Program(Model):
@@ -121,6 +125,10 @@ def templates(self, program_id: str) -> TemplatesService:
121125
http_client=self.http_client, endpoint_params={"program_id": program_id}
122126
)
123127

128+
def terms(self, program_id: str) -> TermService:
129+
"""Return program terms service."""
130+
return TermService(http_client=self.http_client, endpoint_params={"program_id": program_id})
131+
124132

125133
class AsyncProgramsService(
126134
AsyncGetMixin[Program],
@@ -172,3 +180,9 @@ def templates(self, program_id: str) -> AsyncTemplatesService:
172180
return AsyncTemplatesService(
173181
http_client=self.http_client, endpoint_params={"program_id": program_id}
174182
)
183+
184+
def terms(self, program_id: str) -> AsyncTermService:
185+
"""Return async program terms service."""
186+
return AsyncTermService(
187+
http_client=self.http_client, endpoint_params={"program_id": program_id}
188+
)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from mpt_api_client.http import AsyncService, Service
2+
from mpt_api_client.http.mixins import (
3+
AsyncCollectionMixin,
4+
AsyncManagedResourceMixin,
5+
CollectionMixin,
6+
ManagedResourceMixin,
7+
)
8+
from mpt_api_client.models import Model
9+
from mpt_api_client.models.model import BaseModel
10+
from mpt_api_client.resources.program.mixins import (
11+
AsyncPublishableMixin,
12+
PublishableMixin,
13+
)
14+
15+
16+
class Term(Model):
17+
"""Program term resource.
18+
19+
Attributes:
20+
name: Program term name.
21+
description: Program term description.
22+
display_order: Display order of the program term.
23+
status: Program term status.
24+
program: Reference to the program.
25+
audit: Audit information (created, updated events).
26+
"""
27+
28+
name: str | None
29+
description: str | None
30+
display_order: int | None
31+
status: str | None
32+
program: BaseModel | None
33+
audit: BaseModel | None
34+
35+
36+
class TermServiceConfig:
37+
"""Program term service configuration."""
38+
39+
_endpoint = "/public/v1/program/programs/{program_id}/terms"
40+
_model_class = Term
41+
_collection_key = "data"
42+
43+
44+
class TermService(
45+
PublishableMixin[Term],
46+
ManagedResourceMixin[Term],
47+
CollectionMixin[Term],
48+
Service[Term],
49+
TermServiceConfig,
50+
):
51+
"""Program term service."""
52+
53+
54+
class AsyncTermService(
55+
AsyncPublishableMixin[Term],
56+
AsyncManagedResourceMixin[Term],
57+
AsyncCollectionMixin[Term],
58+
AsyncService[Term],
59+
TermServiceConfig,
60+
):
61+
"""Async program term service."""

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ per-file-ignores = [
138138
"tests/e2e/commerce/order/asset/*.py: WPS211 WPS202",
139139
"tests/e2e/commerce/subscription/*.py: WPS202",
140140
"tests/e2e/helpdesk/chats/links/*.py: WPS221 WPS202",
141+
"tests/e2e/program/program/term/*.py: WPS202 WPS204",
141142
"tests/unit/http/test_async_service.py: WPS204 WPS202",
142143
"tests/unit/http/test_resource_accessor.py: WPS204 WPS202 WPS210 WPS219",
143144
"tests/unit/http/test_service.py: WPS204 WPS202",
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import pytest
2+
3+
4+
@pytest.fixture
5+
def term_id(e2e_config):
6+
return e2e_config["program.terms.id"]
7+
8+
9+
@pytest.fixture
10+
def invalid_term_id():
11+
return "PTC-0000-0000-0000"
12+
13+
14+
@pytest.fixture
15+
def term_data():
16+
return {
17+
"name": "E2E Created Program Terms",
18+
"description": "E2E Created Program Terms",
19+
"displayOrder": 100,
20+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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_term(async_mpt_vendor, program_id, term_data):
11+
service = async_mpt_vendor.program.programs.terms(program_id)
12+
term = await service.create(term_data)
13+
yield term
14+
try:
15+
await service.delete(term.id)
16+
except MPTAPIError as error:
17+
print(f"TEARDOWN - Unable to delete term {term.id}: {error.title}") # noqa: WPS421
18+
19+
20+
def test_create_term(created_term):
21+
result = created_term.name == "E2E Created Program Terms"
22+
23+
assert result is True
24+
25+
26+
async def test_update_term(async_mpt_vendor, program_id, created_term):
27+
service = async_mpt_vendor.program.programs.terms(program_id)
28+
update_data = {"name": "E2E Updated Program Terms"}
29+
30+
result = await service.update(created_term.id, update_data)
31+
32+
assert result.name == update_data["name"]
33+
34+
35+
async def test_get_term(async_mpt_vendor, program_id, term_id):
36+
service = async_mpt_vendor.program.programs.terms(program_id)
37+
38+
result = await service.get(term_id)
39+
40+
assert result.id == term_id
41+
42+
43+
async def test_get_invalid_term(async_mpt_vendor, program_id, invalid_term_id):
44+
with pytest.raises(MPTAPIError):
45+
await async_mpt_vendor.program.programs.terms(program_id).get(invalid_term_id)
46+
47+
48+
async def test_delete_term(async_mpt_vendor, program_id, created_term):
49+
term_data = created_term
50+
51+
result = async_mpt_vendor.program.programs.terms(program_id)
52+
53+
await result.delete(term_data.id)
54+
55+
56+
async def test_filter_and_select_terms(async_mpt_vendor, program_id, term_id):
57+
select_fields = ["-description", "-audit"]
58+
filtered_terms = (
59+
async_mpt_vendor.program.programs
60+
.terms(program_id)
61+
.filter(RQLQuery(id=term_id))
62+
.filter(RQLQuery(name="E2E Seeded Program Terms"))
63+
.select(*select_fields)
64+
)
65+
66+
result = [terms async for terms in filtered_terms.iterate()]
67+
68+
assert len(result) == 1
69+
70+
71+
async def test_publish_term(async_mpt_vendor, program_id, created_term):
72+
service = async_mpt_vendor.program.programs.terms(program_id)
73+
74+
result = await service.publish(created_term.id)
75+
76+
assert result.status == "Published"
77+
78+
79+
async def test_unpublish_term(async_mpt_vendor, program_id, created_term):
80+
service = async_mpt_vendor.program.programs.terms(program_id)
81+
await service.publish(created_term.id)
82+
83+
result = await service.unpublish(created_term.id)
84+
85+
assert result.status == "Unpublished"
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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_term(mpt_vendor, program_id, term_data):
11+
service = mpt_vendor.program.programs.terms(program_id)
12+
term = service.create(term_data)
13+
yield term
14+
try:
15+
service.delete(term.id)
16+
except MPTAPIError as error:
17+
print(f"TEARDOWN - Unable to delete term {term.id}: {error.title}") # noqa: WPS421
18+
19+
20+
def test_create_term(created_term):
21+
result = created_term.name == "E2E Created Program Terms"
22+
23+
assert result is True
24+
25+
26+
def test_update_term(mpt_vendor, program_id, created_term):
27+
service = mpt_vendor.program.programs.terms(program_id)
28+
update_data = {"name": "E2E Updated Program Terms"}
29+
30+
result = service.update(created_term.id, update_data)
31+
32+
assert result.name == update_data["name"]
33+
34+
35+
def test_get_term(mpt_vendor, program_id, term_id):
36+
service = mpt_vendor.program.programs.terms(program_id)
37+
38+
result = service.get(term_id)
39+
40+
assert result.id == term_id
41+
42+
43+
def test_get_invalid_term(mpt_vendor, program_id, invalid_term_id):
44+
with pytest.raises(MPTAPIError):
45+
mpt_vendor.program.programs.terms(program_id).get(invalid_term_id)
46+
47+
48+
def test_delete_term(mpt_vendor, program_id, created_term):
49+
term_data = created_term
50+
51+
result = mpt_vendor.program.programs.terms(program_id)
52+
53+
result.delete(term_data.id)
54+
55+
56+
def test_filter_and_select_terms(mpt_vendor, program_id, term_id):
57+
select_fields = ["-description", "-audit"]
58+
filtered_terms = (
59+
mpt_vendor.program.programs
60+
.terms(program_id)
61+
.filter(RQLQuery(id=term_id))
62+
.filter(RQLQuery(name="E2E Seeded Program Terms"))
63+
.select(*select_fields)
64+
)
65+
66+
result = list(filtered_terms.iterate())
67+
68+
assert len(result) == 1
69+
70+
71+
def test_publish_term(mpt_vendor, program_id, created_term):
72+
service = mpt_vendor.program.programs.terms(program_id)
73+
74+
result = service.publish(created_term.id)
75+
76+
assert result.status == "Published"
77+
78+
79+
def test_unpublish_term(mpt_vendor, program_id, created_term):
80+
service = mpt_vendor.program.programs.terms(program_id)
81+
service.publish(created_term.id)
82+
83+
result = service.unpublish(created_term.id)
84+
85+
assert result.status == "Unpublished"

tests/unit/resources/program/test_programs.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
AsyncTemplatesService,
2525
TemplatesService,
2626
)
27+
from mpt_api_client.resources.program.programs_terms import (
28+
AsyncTermService,
29+
TermService,
30+
)
2731

2832

2933
@pytest.fixture
@@ -104,6 +108,7 @@ def test_async_mixins_present(async_programs_service, method):
104108
("parameter_groups", ParameterGroupsService),
105109
("parameters", ParametersService),
106110
("templates", TemplatesService),
111+
("terms", TermService),
107112
],
108113
)
109114
def test_property_services(programs_service, service_method, expected_service_class):
@@ -121,6 +126,7 @@ def test_property_services(programs_service, service_method, expected_service_cl
121126
("parameter_groups", AsyncParameterGroupsService),
122127
("parameters", AsyncParametersService),
123128
("templates", AsyncTemplatesService),
129+
("terms", AsyncTermService),
124130
],
125131
)
126132
def test_async_property_services(async_programs_service, service_method, expected_service_class):

0 commit comments

Comments
 (0)