From e18fe843b30bfd5841d89f28ea1ad25b260fcfe7 Mon Sep 17 00:00:00 2001 From: Robert Segal Date: Mon, 20 Apr 2026 07:06:04 -0600 Subject: [PATCH] Added endpoints and e2e tests for program terms --- e2e_config.test.json | 3 +- mpt_api_client/resources/program/programs.py | 14 +++ .../resources/program/programs_terms.py | 61 +++++++++++++ pyproject.toml | 1 + tests/e2e/program/program/term/conftest.py | 20 +++++ .../program/program/term/test_async_term.py | 85 ++++++++++++++++++ .../program/program/term/test_sync_term.py | 85 ++++++++++++++++++ tests/unit/resources/program/test_programs.py | 6 ++ .../resources/program/test_programs_terms.py | 90 +++++++++++++++++++ 9 files changed, 364 insertions(+), 1 deletion(-) create mode 100644 mpt_api_client/resources/program/programs_terms.py create mode 100644 tests/e2e/program/program/term/conftest.py create mode 100644 tests/e2e/program/program/term/test_async_term.py create mode 100644 tests/e2e/program/program/term/test_sync_term.py create mode 100644 tests/unit/resources/program/test_programs_terms.py diff --git a/e2e_config.test.json b/e2e_config.test.json index 1e037c5a..c9e31284 100644 --- a/e2e_config.test.json +++ b/e2e_config.test.json @@ -75,5 +75,6 @@ "program.parameter.group.id": "PPG-9643-3741-0002", "program.parameter.id": "PPM-9643-3741-0001", "program.program.id": "PRG-9643-3741", - "program.template.id": "PTM-9643-3741-0004" + "program.template.id": "PTM-9643-3741-0004", + "program.terms.id": "PTC-9643-3741-0001" } diff --git a/mpt_api_client/resources/program/programs.py b/mpt_api_client/resources/program/programs.py index df1caad6..fabb756f 100644 --- a/mpt_api_client/resources/program/programs.py +++ b/mpt_api_client/resources/program/programs.py @@ -30,6 +30,10 @@ AsyncTemplatesService, TemplatesService, ) +from mpt_api_client.resources.program.programs_terms import ( + AsyncTermService, + TermService, +) class Program(Model): @@ -121,6 +125,10 @@ def templates(self, program_id: str) -> TemplatesService: http_client=self.http_client, endpoint_params={"program_id": program_id} ) + def terms(self, program_id: str) -> TermService: + """Return program terms service.""" + return TermService(http_client=self.http_client, endpoint_params={"program_id": program_id}) + class AsyncProgramsService( AsyncGetMixin[Program], @@ -172,3 +180,9 @@ def templates(self, program_id: str) -> AsyncTemplatesService: return AsyncTemplatesService( http_client=self.http_client, endpoint_params={"program_id": program_id} ) + + def terms(self, program_id: str) -> AsyncTermService: + """Return async program terms service.""" + return AsyncTermService( + http_client=self.http_client, endpoint_params={"program_id": program_id} + ) diff --git a/mpt_api_client/resources/program/programs_terms.py b/mpt_api_client/resources/program/programs_terms.py new file mode 100644 index 00000000..85dedee6 --- /dev/null +++ b/mpt_api_client/resources/program/programs_terms.py @@ -0,0 +1,61 @@ +from mpt_api_client.http import AsyncService, Service +from mpt_api_client.http.mixins import ( + AsyncCollectionMixin, + AsyncManagedResourceMixin, + CollectionMixin, + ManagedResourceMixin, +) +from mpt_api_client.models import Model +from mpt_api_client.models.model import BaseModel +from mpt_api_client.resources.program.mixins import ( + AsyncPublishableMixin, + PublishableMixin, +) + + +class Term(Model): + """Program term resource. + + Attributes: + name: Program term name. + description: Program term description. + display_order: Display order of the program term. + status: Program term status. + program: Reference to the program. + audit: Audit information (created, updated events). + """ + + name: str | None + description: str | None + display_order: int | None + status: str | None + program: BaseModel | None + audit: BaseModel | None + + +class TermServiceConfig: + """Program term service configuration.""" + + _endpoint = "/public/v1/program/programs/{program_id}/terms" + _model_class = Term + _collection_key = "data" + + +class TermService( + PublishableMixin[Term], + ManagedResourceMixin[Term], + CollectionMixin[Term], + Service[Term], + TermServiceConfig, +): + """Program term service.""" + + +class AsyncTermService( + AsyncPublishableMixin[Term], + AsyncManagedResourceMixin[Term], + AsyncCollectionMixin[Term], + AsyncService[Term], + TermServiceConfig, +): + """Async program term service.""" diff --git a/pyproject.toml b/pyproject.toml index d6ed71af..ebd8d39c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -138,6 +138,7 @@ per-file-ignores = [ "tests/e2e/commerce/order/asset/*.py: WPS211 WPS202", "tests/e2e/commerce/subscription/*.py: WPS202", "tests/e2e/helpdesk/chats/links/*.py: WPS221 WPS202", + "tests/e2e/program/program/term/*.py: WPS202 WPS204", "tests/unit/http/test_async_service.py: WPS204 WPS202", "tests/unit/http/test_resource_accessor.py: WPS204 WPS202 WPS210 WPS219", "tests/unit/http/test_service.py: WPS204 WPS202", diff --git a/tests/e2e/program/program/term/conftest.py b/tests/e2e/program/program/term/conftest.py new file mode 100644 index 00000000..6747289c --- /dev/null +++ b/tests/e2e/program/program/term/conftest.py @@ -0,0 +1,20 @@ +import pytest + + +@pytest.fixture +def term_id(e2e_config): + return e2e_config["program.terms.id"] + + +@pytest.fixture +def invalid_term_id(): + return "PTC-0000-0000-0000" + + +@pytest.fixture +def term_data(): + return { + "name": "E2E Created Program Terms", + "description": "E2E Created Program Terms", + "displayOrder": 100, + } diff --git a/tests/e2e/program/program/term/test_async_term.py b/tests/e2e/program/program/term/test_async_term.py new file mode 100644 index 00000000..5d3abae2 --- /dev/null +++ b/tests/e2e/program/program/term/test_async_term.py @@ -0,0 +1,85 @@ +import pytest + +from mpt_api_client.exceptions import MPTAPIError +from mpt_api_client.rql.query_builder import RQLQuery + +pytestmark = [pytest.mark.flaky] + + +@pytest.fixture +async def created_term(async_mpt_vendor, program_id, term_data): + service = async_mpt_vendor.program.programs.terms(program_id) + term = await service.create(term_data) + yield term + try: + await service.delete(term.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete term {term.id}: {error.title}") # noqa: WPS421 + + +def test_create_term(created_term): + result = created_term.name == "E2E Created Program Terms" + + assert result is True + + +async def test_update_term(async_mpt_vendor, program_id, created_term): + service = async_mpt_vendor.program.programs.terms(program_id) + update_data = {"name": "E2E Updated Program Terms"} + + result = await service.update(created_term.id, update_data) + + assert result.name == update_data["name"] + + +async def test_get_term(async_mpt_vendor, program_id, term_id): + service = async_mpt_vendor.program.programs.terms(program_id) + + result = await service.get(term_id) + + assert result.id == term_id + + +async def test_get_invalid_term(async_mpt_vendor, program_id, invalid_term_id): + with pytest.raises(MPTAPIError): + await async_mpt_vendor.program.programs.terms(program_id).get(invalid_term_id) + + +async def test_delete_term(async_mpt_vendor, program_id, created_term): + term_data = created_term + + result = async_mpt_vendor.program.programs.terms(program_id) + + await result.delete(term_data.id) + + +async def test_filter_and_select_terms(async_mpt_vendor, program_id, term_id): + select_fields = ["-description", "-audit"] + filtered_terms = ( + async_mpt_vendor.program.programs + .terms(program_id) + .filter(RQLQuery(id=term_id)) + .filter(RQLQuery(name="E2E Seeded Program Terms")) + .select(*select_fields) + ) + + result = [terms async for terms in filtered_terms.iterate()] + + assert len(result) == 1 + + +async def test_publish_term(async_mpt_vendor, program_id, created_term): + service = async_mpt_vendor.program.programs.terms(program_id) + + result = await service.publish(created_term.id) + + assert result.status == "Published" + + +async def test_unpublish_term(async_mpt_vendor, program_id, created_term): + service = async_mpt_vendor.program.programs.terms(program_id) + await service.publish(created_term.id) + + result = await service.unpublish(created_term.id) + + assert result.status == "Unpublished" diff --git a/tests/e2e/program/program/term/test_sync_term.py b/tests/e2e/program/program/term/test_sync_term.py new file mode 100644 index 00000000..591578c5 --- /dev/null +++ b/tests/e2e/program/program/term/test_sync_term.py @@ -0,0 +1,85 @@ +import pytest + +from mpt_api_client.exceptions import MPTAPIError +from mpt_api_client.rql.query_builder import RQLQuery + +pytestmark = [pytest.mark.flaky] + + +@pytest.fixture +def created_term(mpt_vendor, program_id, term_data): + service = mpt_vendor.program.programs.terms(program_id) + term = service.create(term_data) + yield term + try: + service.delete(term.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete term {term.id}: {error.title}") # noqa: WPS421 + + +def test_create_term(created_term): + result = created_term.name == "E2E Created Program Terms" + + assert result is True + + +def test_update_term(mpt_vendor, program_id, created_term): + service = mpt_vendor.program.programs.terms(program_id) + update_data = {"name": "E2E Updated Program Terms"} + + result = service.update(created_term.id, update_data) + + assert result.name == update_data["name"] + + +def test_get_term(mpt_vendor, program_id, term_id): + service = mpt_vendor.program.programs.terms(program_id) + + result = service.get(term_id) + + assert result.id == term_id + + +def test_get_invalid_term(mpt_vendor, program_id, invalid_term_id): + with pytest.raises(MPTAPIError): + mpt_vendor.program.programs.terms(program_id).get(invalid_term_id) + + +def test_delete_term(mpt_vendor, program_id, created_term): + term_data = created_term + + result = mpt_vendor.program.programs.terms(program_id) + + result.delete(term_data.id) + + +def test_filter_and_select_terms(mpt_vendor, program_id, term_id): + select_fields = ["-description", "-audit"] + filtered_terms = ( + mpt_vendor.program.programs + .terms(program_id) + .filter(RQLQuery(id=term_id)) + .filter(RQLQuery(name="E2E Seeded Program Terms")) + .select(*select_fields) + ) + + result = list(filtered_terms.iterate()) + + assert len(result) == 1 + + +def test_publish_term(mpt_vendor, program_id, created_term): + service = mpt_vendor.program.programs.terms(program_id) + + result = service.publish(created_term.id) + + assert result.status == "Published" + + +def test_unpublish_term(mpt_vendor, program_id, created_term): + service = mpt_vendor.program.programs.terms(program_id) + service.publish(created_term.id) + + result = service.unpublish(created_term.id) + + assert result.status == "Unpublished" diff --git a/tests/unit/resources/program/test_programs.py b/tests/unit/resources/program/test_programs.py index 0edfe1e2..20170b95 100644 --- a/tests/unit/resources/program/test_programs.py +++ b/tests/unit/resources/program/test_programs.py @@ -24,6 +24,10 @@ AsyncTemplatesService, TemplatesService, ) +from mpt_api_client.resources.program.programs_terms import ( + AsyncTermService, + TermService, +) @pytest.fixture @@ -104,6 +108,7 @@ def test_async_mixins_present(async_programs_service, method): ("parameter_groups", ParameterGroupsService), ("parameters", ParametersService), ("templates", TemplatesService), + ("terms", TermService), ], ) 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 ("parameter_groups", AsyncParameterGroupsService), ("parameters", AsyncParametersService), ("templates", AsyncTemplatesService), + ("terms", AsyncTermService), ], ) def test_async_property_services(async_programs_service, service_method, expected_service_class): diff --git a/tests/unit/resources/program/test_programs_terms.py b/tests/unit/resources/program/test_programs_terms.py new file mode 100644 index 00000000..75f0e7a2 --- /dev/null +++ b/tests/unit/resources/program/test_programs_terms.py @@ -0,0 +1,90 @@ +from typing import Any + +import pytest + +from mpt_api_client.models.model import BaseModel +from mpt_api_client.resources.program.programs_terms import ( + AsyncTermService, + Term, + TermService, +) + + +@pytest.fixture +def term_service(http_client: Any) -> TermService: + return TermService(http_client=http_client, endpoint_params={"program_id": "PRG-001"}) + + +@pytest.fixture +def async_term_service(async_http_client: Any) -> AsyncTermService: + return AsyncTermService( + http_client=async_http_client, endpoint_params={"program_id": "PRG-001"} + ) + + +@pytest.fixture +def term_data(): + return { + "id": "PTC-001", + "name": "Terms of Service", + "description": "Standard terms", + "displayOrder": 1, + "status": "Active", + "program": {"id": "PRG-001", "name": "My Program"}, + "audit": {"created": {"at": "2024-01-01T00:00:00Z"}}, + } + + +def test_endpoint(term_service: TermService) -> None: + result = term_service.path == "/public/v1/program/programs/PRG-001/terms" + + assert result is True + + +def test_async_endpoint(async_term_service: AsyncTermService) -> None: + result = async_term_service.path == "/public/v1/program/programs/PRG-001/terms" + + assert result is True + + +@pytest.mark.parametrize( + "method", ["get", "create", "delete", "update", "publish", "unpublish", "iterate"] +) +def test_methods_present(term_service: TermService, method: str) -> None: + result = hasattr(term_service, method) + + assert result is True + + +@pytest.mark.parametrize( + "method", ["get", "create", "delete", "update", "publish", "unpublish", "iterate"] +) +def test_async_methods_present(async_term_service: AsyncTermService, method: str) -> None: + result = hasattr(async_term_service, method) + + assert result is True + + +def test_term_primitive_fields(term_data: dict) -> None: + result = Term(term_data) + + assert result.to_dict() == term_data + + +def test_term_nested_fields_are_base_models(term_data: dict) -> None: + result = Term(term_data) + + assert isinstance(result.program, BaseModel) + assert isinstance(result.audit, BaseModel) + + +def test_term_optional_fields_absent() -> None: + result = Term({"id": "PTC-001"}) + + assert result.id == "PTC-001" + assert not hasattr(result, "name") + assert not hasattr(result, "description") + assert not hasattr(result, "display_order") + assert not hasattr(result, "status") + assert not hasattr(result, "program") + assert not hasattr(result, "audit")