From 458aeea83bd65643b0ccf8a63eb90bf327767113 Mon Sep 17 00:00:00 2001 From: Robert Segal Date: Wed, 15 Apr 2026 15:18:30 -0600 Subject: [PATCH] Added endpoints and e2e tests for program programs --- e2e_config.test.json | 3 +- mpt_api_client/mpt_client.py | 12 ++ mpt_api_client/resources/__init__.py | 3 + mpt_api_client/resources/program/__init__.py | 6 + .../resources/program/mixins/__init__.py | 9 + .../program/mixins/publishable_mixin.py | 45 +++++ mpt_api_client/resources/program/program.py | 26 +++ mpt_api_client/resources/program/programs.py | 94 +++++++++ pyproject.toml | 1 + tests/e2e/program/__init__.py | 0 tests/e2e/program/conftest.py | 6 + tests/e2e/program/program/conftest.py | 16 ++ .../e2e/program/program/test_async_program.py | 74 +++++++ .../e2e/program/program/test_sync_program.py | 74 +++++++ .../program/mixin/test_publishable_mixin.py | 159 +++++++++++++++ tests/unit/resources/program/test_program.py | 54 ++++++ tests/unit/resources/program/test_programs.py | 181 ++++++++++++++++++ tests/unit/test_mpt_client.py | 4 + 18 files changed, 766 insertions(+), 1 deletion(-) create mode 100644 mpt_api_client/resources/program/__init__.py create mode 100644 mpt_api_client/resources/program/mixins/__init__.py create mode 100644 mpt_api_client/resources/program/mixins/publishable_mixin.py create mode 100644 mpt_api_client/resources/program/program.py create mode 100644 mpt_api_client/resources/program/programs.py create mode 100644 tests/e2e/program/__init__.py create mode 100644 tests/e2e/program/conftest.py create mode 100644 tests/e2e/program/program/conftest.py create mode 100644 tests/e2e/program/program/test_async_program.py create mode 100644 tests/e2e/program/program/test_sync_program.py create mode 100644 tests/unit/resources/program/mixin/test_publishable_mixin.py create mode 100644 tests/unit/resources/program/test_program.py create mode 100644 tests/unit/resources/program/test_programs.py diff --git a/e2e_config.test.json b/e2e_config.test.json index b999baef..00bac28b 100644 --- a/e2e_config.test.json +++ b/e2e_config.test.json @@ -68,5 +68,6 @@ "notifications.message.id": "MSG-0000-6215-1019-0139", "notifications.subscriber.id": "NTS-0829-7123-7123", "integration.extension.id": "EXT-6587-4477", - "integration.term.id": "ETC-6587-4477-0062" + "integration.term.id": "ETC-6587-4477-0062", + "program.program.id": "PRG-9643-3741" } diff --git a/mpt_api_client/mpt_client.py b/mpt_api_client/mpt_client.py index b5ebc258..0a569f05 100644 --- a/mpt_api_client/mpt_client.py +++ b/mpt_api_client/mpt_client.py @@ -12,6 +12,7 @@ AsyncHelpdesk, AsyncIntegration, AsyncNotifications, + AsyncProgram, Audit, Billing, Catalog, @@ -20,6 +21,7 @@ Helpdesk, Integration, Notifications, + Program, ) @@ -97,6 +99,11 @@ def integration(self) -> AsyncIntegration: """Integration MPT API Client.""" return AsyncIntegration(http_client=self.http_client) + @property + def program(self) -> AsyncProgram: + """Program MPT API Client.""" + return AsyncProgram(http_client=self.http_client) + class MPTClient: """MPT API Client.""" @@ -176,3 +183,8 @@ def exchange(self) -> Exchange: def integration(self) -> Integration: """Integration MPT API Client.""" return Integration(http_client=self.http_client) + + @property + def program(self) -> Program: + """Program MPT API Client.""" + return Program(http_client=self.http_client) diff --git a/mpt_api_client/resources/__init__.py b/mpt_api_client/resources/__init__.py index 76daeb78..dd686aa6 100644 --- a/mpt_api_client/resources/__init__.py +++ b/mpt_api_client/resources/__init__.py @@ -7,6 +7,7 @@ from mpt_api_client.resources.helpdesk import AsyncHelpdesk, Helpdesk from mpt_api_client.resources.integration import AsyncIntegration, Integration from mpt_api_client.resources.notifications import AsyncNotifications, Notifications +from mpt_api_client.resources.program import AsyncProgram, Program __all__ = [ # noqa: WPS410 "Accounts", @@ -19,6 +20,7 @@ "AsyncHelpdesk", "AsyncIntegration", "AsyncNotifications", + "AsyncProgram", "Audit", "Billing", "Catalog", @@ -27,4 +29,5 @@ "Helpdesk", "Integration", "Notifications", + "Program", ] diff --git a/mpt_api_client/resources/program/__init__.py b/mpt_api_client/resources/program/__init__.py new file mode 100644 index 00000000..7b3c62e5 --- /dev/null +++ b/mpt_api_client/resources/program/__init__.py @@ -0,0 +1,6 @@ +from mpt_api_client.resources.program.program import AsyncProgram, Program + +__all__ = [ # noqa: WPS410 + "AsyncProgram", + "Program", +] diff --git a/mpt_api_client/resources/program/mixins/__init__.py b/mpt_api_client/resources/program/mixins/__init__.py new file mode 100644 index 00000000..551e3e00 --- /dev/null +++ b/mpt_api_client/resources/program/mixins/__init__.py @@ -0,0 +1,9 @@ +from mpt_api_client.resources.program.mixins.publishable_mixin import ( + AsyncPublishableMixin, + PublishableMixin, +) + +__all__ = [ # noqa: WPS410 + "AsyncPublishableMixin", + "PublishableMixin", +] diff --git a/mpt_api_client/resources/program/mixins/publishable_mixin.py b/mpt_api_client/resources/program/mixins/publishable_mixin.py new file mode 100644 index 00000000..b85b6b25 --- /dev/null +++ b/mpt_api_client/resources/program/mixins/publishable_mixin.py @@ -0,0 +1,45 @@ +from mpt_api_client.models import ResourceData + + +class PublishableMixin[Model]: + """Publishable mixin adds the ability to publish and unpublish.""" + + def publish(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Update state to Published. + + Args: + resource_id: Resource ID + resource_data: Resource data will be updated + """ + return self._resource(resource_id).post("publish", json=resource_data) # type: ignore[attr-defined, no-any-return] + + def unpublish(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Update state to Unpublished. + + Args: + resource_id: Resource ID + resource_data: Resource data will be updated + """ + return self._resource(resource_id).post("unpublish", json=resource_data) # type: ignore[attr-defined, no-any-return] + + +class AsyncPublishableMixin[Model]: + """Publishable mixin adds the ability to publish and unpublish.""" + + async def publish(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Update state to Published. + + Args: + resource_id: Resource ID + resource_data: Resource data will be updated + """ + return await self._resource(resource_id).post("publish", json=resource_data) # type: ignore[attr-defined, no-any-return] + + async def unpublish(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Update state to Unpublished. + + Args: + resource_id: Resource ID + resource_data: Resource data will be updated + """ + return await self._resource(resource_id).post("unpublish", json=resource_data) # type: ignore[attr-defined, no-any-return] diff --git a/mpt_api_client/resources/program/program.py b/mpt_api_client/resources/program/program.py new file mode 100644 index 00000000..2b31cba3 --- /dev/null +++ b/mpt_api_client/resources/program/program.py @@ -0,0 +1,26 @@ +from mpt_api_client.http import AsyncHTTPClient, HTTPClient +from mpt_api_client.resources.program.programs import AsyncProgramsService, ProgramsService + + +class Program: + """Program MPT API Module.""" + + def __init__(self, *, http_client: HTTPClient): + self.http_client = http_client + + @property + def programs(self) -> ProgramsService: + """Programs service.""" + return ProgramsService(http_client=self.http_client) + + +class AsyncProgram: + """Program MPT API Module.""" + + def __init__(self, *, http_client: AsyncHTTPClient): + self.http_client = http_client + + @property + def programs(self) -> AsyncProgramsService: + """Programs service.""" + return AsyncProgramsService(http_client=self.http_client) diff --git a/mpt_api_client/resources/program/programs.py b/mpt_api_client/resources/program/programs.py new file mode 100644 index 00000000..792fee4d --- /dev/null +++ b/mpt_api_client/resources/program/programs.py @@ -0,0 +1,94 @@ +from mpt_api_client.http import AsyncService, Service +from mpt_api_client.http.mixins.collection_mixin import AsyncCollectionMixin, CollectionMixin +from mpt_api_client.http.mixins.create_file_mixin import AsyncCreateFileMixin, CreateFileMixin +from mpt_api_client.http.mixins.delete_mixin import AsyncDeleteMixin, DeleteMixin +from mpt_api_client.http.mixins.get_mixin import AsyncGetMixin, GetMixin +from mpt_api_client.http.mixins.update_file_mixin import AsyncUpdateFileMixin, UpdateFileMixin +from mpt_api_client.models import Model +from mpt_api_client.models.model import BaseModel, ResourceData +from mpt_api_client.resources.program.mixins import ( + AsyncPublishableMixin, + PublishableMixin, +) + + +class Program(Model): + """Program resource. + + Attributes: + name: Program name. + website: Program website. + eligibility: Eligibility criteria for the program. + applicable_to: Applicable products or services for the program. + icon: Program icon URL. + status: Program status. + vendor: Reference to the vendor account associated with the program. + settings: Program settings. + statistics: Program statistics and performance metrics. + audit: Audit information related to the program (created, updated events). + """ + + name: str | None + website: str | None + eligibility: BaseModel | None + applicable_to: str | None + icon: str | None + status: str | None + vendor: BaseModel | None + settings: BaseModel | None + statistics: BaseModel | None + audit: BaseModel | None + + +class ProgramsServiceConfig: + """Programs service configuration.""" + + _endpoint = "/public/v1/program/programs" + _model_class = Program + _collection_key = "data" + _upload_file_key = "icon" + _upload_data_key = "program" + + +class ProgramsService( + GetMixin[Program], + CreateFileMixin[Program], + UpdateFileMixin[Program], + DeleteMixin, + PublishableMixin[Program], + CollectionMixin[Program], + Service[Program], + ProgramsServiceConfig, +): + """Programs service.""" + + def update_settings(self, program_id: str, settings: ResourceData) -> Program: + """Update program settings. + + Args: + program_id: Program ID + settings: Settings data to be updated + """ + return self._resource(program_id).put("settings", json=settings) + + +class AsyncProgramsService( + AsyncGetMixin[Program], + AsyncCreateFileMixin[Program], + AsyncUpdateFileMixin[Program], + AsyncDeleteMixin, + AsyncPublishableMixin[Program], + AsyncCollectionMixin[Program], + AsyncService[Program], + ProgramsServiceConfig, +): + """Async programs service.""" + + async def update_settings(self, program_id: str, settings: ResourceData) -> Program: + """Update program settings. + + Args: + program_id: Program ID + settings: Settings data to be updated + """ + return await self._resource(program_id).put("settings", json=settings) diff --git a/pyproject.toml b/pyproject.toml index 0fb83bc5..58abc7dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -148,6 +148,7 @@ per-file-ignores = [ "tests/unit/resources/integration/*.py: WPS202 WPS210 WPS218 WPS453", "tests/unit/resources/integration/mixins/*.py: WPS453 WPS202", "tests/unit/resources/commerce/*.py: WPS202 WPS204", + "tests/unit/resources/program/*.py: WPS202 WPS210 WPS218", "tests/unit/test_mpt_client.py: WPS235", "tests/*: WPS432 WPS202", "tests/unit/resources/exchange/*.py: WPS202 WPS204 WPS210", diff --git a/tests/e2e/program/__init__.py b/tests/e2e/program/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/e2e/program/conftest.py b/tests/e2e/program/conftest.py new file mode 100644 index 00000000..bb360235 --- /dev/null +++ b/tests/e2e/program/conftest.py @@ -0,0 +1,6 @@ +import pytest + + +@pytest.fixture +def program_id(e2e_config): + return e2e_config.get("program.program.id") diff --git a/tests/e2e/program/program/conftest.py b/tests/e2e/program/program/conftest.py new file mode 100644 index 00000000..b7c064f2 --- /dev/null +++ b/tests/e2e/program/program/conftest.py @@ -0,0 +1,16 @@ +import pytest + + +@pytest.fixture +def program_data(): + return { + "name": "E2E Created Program", + "website": "www.example.com", + "eligibility": {"client": True, "partner": True}, + "applicableTo": "Licensee", + } + + +@pytest.fixture +def invalid_program_id(): + return "PRG-0000-0000" diff --git a/tests/e2e/program/program/test_async_program.py b/tests/e2e/program/program/test_async_program.py new file mode 100644 index 00000000..81c9c299 --- /dev/null +++ b/tests/e2e/program/program/test_async_program.py @@ -0,0 +1,74 @@ +import pytest + +from mpt_api_client import RQLQuery +from mpt_api_client.exceptions import MPTAPIError + +pytestmark = [pytest.mark.flaky] + + +@pytest.fixture +async def created_program(async_mpt_vendor, program_data, logo_fd): + program = await async_mpt_vendor.program.programs.create(program_data, file=logo_fd) + + yield program + + try: + await async_mpt_vendor.program.programs.delete(program.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete program {program.id}: {error.title}") # noqa: WPS421 + + +def test_create_program(created_program, program_data): + result = created_program.name == program_data["name"] + + assert result is True + + +async def test_update_program(async_mpt_vendor, created_program): + update_data = {"name": "E2E Updated Program"} + + result = await async_mpt_vendor.program.programs.update(created_program.id, update_data) + + assert result.name == update_data["name"] + + +async def test_get_program(async_mpt_vendor, program_id): + result = await async_mpt_vendor.program.programs.get(program_id) + + assert result.id == program_id + + +async def test_filter_and_select_programs(async_mpt_vendor, program_id): + select_fields = ["-icon", "-revision", "-audit"] + filtered_programs = ( + async_mpt_vendor.program.programs + .filter(RQLQuery(id=program_id)) + .filter(RQLQuery(name="E2E Seeded Program")) + .select(*select_fields) + ) + + result = [program async for program in filtered_programs.iterate()] + + assert len(result) == 1 + + +async def test_delete_program(async_mpt_vendor, created_program): + program_data = created_program + + result = async_mpt_vendor.program.programs + + await result.delete(program_data.id) + + +async def test_publish_program(async_mpt_vendor, created_program): + result = await async_mpt_vendor.program.programs.publish(created_program.id) + + assert result is not None + + +async def test_unpublish_program(async_mpt_vendor, created_program): + await async_mpt_vendor.program.programs.publish(created_program.id) + + result = await async_mpt_vendor.program.programs.unpublish(created_program.id) + + assert result is not None diff --git a/tests/e2e/program/program/test_sync_program.py b/tests/e2e/program/program/test_sync_program.py new file mode 100644 index 00000000..421da637 --- /dev/null +++ b/tests/e2e/program/program/test_sync_program.py @@ -0,0 +1,74 @@ +import pytest + +from mpt_api_client import RQLQuery +from mpt_api_client.exceptions import MPTAPIError + +pytestmark = [pytest.mark.flaky] + + +@pytest.fixture +def created_program(mpt_vendor, program_data, logo_fd): + program = mpt_vendor.program.programs.create(program_data, file=logo_fd) + + yield program + + try: + mpt_vendor.program.programs.delete(program.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete program {program.id}: {error.title}") # noqa: WPS421 + + +def test_create_program(created_program, program_data): + result = created_program.name == program_data["name"] + + assert result is True + + +def test_update_program(mpt_vendor, created_program): + update_data = {"name": "E2E Updated Program"} + + result = mpt_vendor.program.programs.update(created_program.id, update_data) + + assert result.name == update_data["name"] + + +def test_get_program(mpt_vendor, program_id): + result = mpt_vendor.program.programs.get(program_id) + + assert result.id == program_id + + +def test_filter_and_select_programs(mpt_vendor, program_id): + select_fields = ["-icon", "-revision", "-audit"] + filtered_programs = ( + mpt_vendor.program.programs + .filter(RQLQuery(id=program_id)) + .filter(RQLQuery(name="E2E Seeded Program")) + .select(*select_fields) + ) + + result = list(filtered_programs.iterate()) + + assert len(result) == 1 + + +def test_delete_program(mpt_vendor, created_program): + program_data = created_program + + result = mpt_vendor.program.programs + + result.delete(program_data.id) + + +def test_publish_program(mpt_vendor, created_program): + result = mpt_vendor.program.programs.publish(created_program.id) + + assert result is not None + + +def test_unpublish_program(mpt_vendor, created_program): + mpt_vendor.program.programs.publish(created_program.id) + + result = mpt_vendor.program.programs.unpublish(created_program.id) + + assert result is not None diff --git a/tests/unit/resources/program/mixin/test_publishable_mixin.py b/tests/unit/resources/program/mixin/test_publishable_mixin.py new file mode 100644 index 00000000..5d5a7e13 --- /dev/null +++ b/tests/unit/resources/program/mixin/test_publishable_mixin.py @@ -0,0 +1,159 @@ +import httpx +import pytest +import respx + +from mpt_api_client.http.async_service import AsyncService +from mpt_api_client.http.service import Service +from mpt_api_client.resources.program.mixins import ( + AsyncPublishableMixin, + PublishableMixin, +) +from tests.unit.conftest import DummyModel + + +class DummyPublishableService( + PublishableMixin[DummyModel], + Service[DummyModel], +): + _endpoint = "/public/v1/dummy/publishable/" + _model_class = DummyModel + _collection_key = "data" + + +class DummyAsyncPublishableService( + AsyncPublishableMixin[DummyModel], + AsyncService[DummyModel], +): + _endpoint = "/public/v1/dummy/publishable/" + _model_class = DummyModel + _collection_key = "data" + + +@pytest.fixture +def publishable_service(http_client): + return DummyPublishableService(http_client=http_client) + + +@pytest.fixture +def async_publishable_service(async_http_client): + return DummyAsyncPublishableService(http_client=async_http_client) + + +@pytest.mark.parametrize( + ("action", "input_status"), + [ + ("publish", {"id": "PRG-123", "status": "update"}), + ("unpublish", {"id": "PRG-123", "status": "update"}), + ], +) +def test_custom_resource_actions(publishable_service, action, input_status): + request_expected_content = b'{"id":"PRG-123","status":"update"}' + response_expected_data = {"id": "PRG-123", "status": "new_status"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/dummy/publishable/PRG-123/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + + result = getattr(publishable_service, action)("PRG-123", input_status) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + assert request.content == request_expected_content + assert result.to_dict() == response_expected_data + assert isinstance(result, DummyModel) + + +@pytest.mark.parametrize( + ("action"), + [ + ("publish"), + ("unpublish"), + ], +) +def test_custom_resource_actions_no_data(publishable_service, action): + request_expected_content = b"" + response_expected_data = {"id": "PRG-123", "status": "new_status"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/dummy/publishable/PRG-123/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + + result = getattr(publishable_service, action)("PRG-123") + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + assert request.content == request_expected_content + assert result.to_dict() == response_expected_data + assert isinstance(result, DummyModel) + + +@pytest.mark.parametrize( + ("action", "input_status"), + [ + ("publish", {"id": "PRG-123", "status": "update"}), + ("unpublish", {"id": "PRG-123", "status": "update"}), + ], +) +async def test_async_custom_resource_actions(async_publishable_service, action, input_status): + request_expected_content = b'{"id":"PRG-123","status":"update"}' + response_expected_data = {"id": "PRG-123", "status": "new_status"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/dummy/publishable/PRG-123/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + + result = await getattr(async_publishable_service, action)("PRG-123", input_status) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + assert request.content == request_expected_content + assert result.to_dict() == response_expected_data + assert isinstance(result, DummyModel) + + +@pytest.mark.parametrize( + ("action"), + [ + ("publish"), + ("unpublish"), + ], +) +async def test_async_custom_resource_actions_no_data(async_publishable_service, action): + request_expected_content = b"" + response_expected_data = {"id": "PRG-123", "status": "new_status"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/dummy/publishable/PRG-123/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + + result = await getattr(async_publishable_service, action)("PRG-123") + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + assert request.content == request_expected_content + assert result.to_dict() == response_expected_data + assert isinstance(result, DummyModel) diff --git a/tests/unit/resources/program/test_program.py b/tests/unit/resources/program/test_program.py new file mode 100644 index 00000000..6e1a8d97 --- /dev/null +++ b/tests/unit/resources/program/test_program.py @@ -0,0 +1,54 @@ +import pytest + +from mpt_api_client.resources.program.program import AsyncProgram, Program +from mpt_api_client.resources.program.programs import AsyncProgramsService, ProgramsService + + +@pytest.fixture +def program(http_client): + return Program(http_client=http_client) + + +@pytest.fixture +def async_program(async_http_client): + return AsyncProgram(http_client=async_http_client) + + +@pytest.mark.parametrize( + ("property_name", "expected_service_class"), + [ + ("programs", ProgramsService), + ], +) +def test_program_properties(program, property_name, expected_service_class): + result = getattr(program, property_name) + + assert isinstance(result, expected_service_class) + assert result.http_client is program.http_client + + +@pytest.mark.parametrize( + ("property_name", "expected_service_class"), + [ + ("programs", AsyncProgramsService), + ], +) +def test_async_program_properties(async_program, property_name, expected_service_class): + result = getattr(async_program, property_name) + + assert isinstance(result, expected_service_class) + assert result.http_client is async_program.http_client + + +def test_program_initialization(http_client): + result = Program(http_client=http_client) + + assert result.http_client is http_client + assert isinstance(result, Program) + + +def test_async_program_initialization(async_http_client): + result = AsyncProgram(http_client=async_http_client) + + assert result.http_client is async_http_client + assert isinstance(result, AsyncProgram) diff --git a/tests/unit/resources/program/test_programs.py b/tests/unit/resources/program/test_programs.py new file mode 100644 index 00000000..2428e1b7 --- /dev/null +++ b/tests/unit/resources/program/test_programs.py @@ -0,0 +1,181 @@ +import httpx +import pytest +import respx + +from mpt_api_client.models.model import BaseModel +from mpt_api_client.resources.program.programs import AsyncProgramsService, Program, ProgramsService + + +@pytest.fixture +def programs_service(http_client): + return ProgramsService(http_client=http_client) + + +@pytest.fixture +def async_programs_service(async_http_client): + return AsyncProgramsService(http_client=async_http_client) + + +@pytest.fixture +def program_settings_data(): + return {"settingKey": "setting_value"} + + +@pytest.fixture +def program_data(program_settings_data): + return { + "id": "PRG-123", + "name": "Test Program", + "website": "https://example.com", + "eligibility": {"client": True, "partner": False}, + "status": "Active", + "applicableTo": "Licensee", + "settings": program_settings_data, + "vendor": {"id": "ACC-001", "name": "Vendor"}, + "icon": "https://example.com/icon.png", + "statistics": {"certificates": 1}, + "audit": {"created": {"at": "2024-01-01T00:00:00Z"}}, + } + + +@pytest.mark.parametrize( + "method", + [ + "get", + "create", + "update", + "delete", + "publish", + "unpublish", + "update_settings", + "iterate", + ], +) +def test_mixins_present(programs_service, method): + result = hasattr(programs_service, method) + + assert result is True + + +@pytest.mark.parametrize( + "method", + [ + "get", + "create", + "update", + "delete", + "publish", + "unpublish", + "update_settings", + "iterate", + ], +) +def test_async_mixins_present(async_programs_service, method): + result = hasattr(async_programs_service, method) + + assert result is True + + +def test_update_settings(programs_service, program_settings_data): + program_id = "PRG-123" + expected_response = {"id": program_id, "settings": program_settings_data} + with respx.mock: + mock_route = respx.put( + f"https://api.example.com/public/v1/program/programs/{program_id}/settings" + ).mock(return_value=httpx.Response(status_code=httpx.codes.OK, json=expected_response)) + + result = programs_service.update_settings(program_id, program_settings_data) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + assert request.method == "PUT" + assert request.url.path == f"/public/v1/program/programs/{program_id}/settings" + assert result.to_dict() == expected_response + + +async def test_async_update_settings(async_programs_service, program_settings_data): + program_id = "PRG-123" + expected_response = {"id": program_id, "settings": program_settings_data} + with respx.mock: + mock_route = respx.put( + f"https://api.example.com/public/v1/program/programs/{program_id}/settings" + ).mock(return_value=httpx.Response(status_code=httpx.codes.OK, json=expected_response)) + + result = await async_programs_service.update_settings(program_id, program_settings_data) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + assert request.method == "PUT" + assert request.url.path == f"/public/v1/program/programs/{program_id}/settings" + assert result.to_dict() == expected_response + + +def test_sync_program_update(programs_service, tmp_path): + program_id = "PRG-123" + update_data = {"name": "Updated Program"} + expected_response = {"id": program_id, "name": "Updated Program"} + icon_path = tmp_path / "icon.png" + icon_path.write_bytes(b"fake image data") + with icon_path.open("rb") as icon_file, respx.mock: + mock_route = respx.put( + f"https://api.example.com/public/v1/program/programs/{program_id}" + ).mock(return_value=httpx.Response(status_code=httpx.codes.OK, json=expected_response)) + + result = programs_service.update(program_id, update_data, file=icon_file) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + assert request.method == "PUT" + assert request.url.path == f"/public/v1/program/programs/{program_id}" + assert result.to_dict() == expected_response + + +async def test_async_program_update(async_programs_service, tmp_path): + program_id = "PRG-123" + update_data = {"name": "Updated Program"} + expected_response = {"id": program_id, "name": "Updated Program"} + icon_path = tmp_path / "icon.png" + icon_path.write_bytes(b"fake image data") + with icon_path.open("rb") as icon_file, respx.mock: + mock_route = respx.put( + f"https://api.example.com/public/v1/program/programs/{program_id}" + ).mock(return_value=httpx.Response(status_code=httpx.codes.OK, json=expected_response)) + + result = await async_programs_service.update(program_id, update_data, file=icon_file) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + assert request.method == "PUT" + assert request.url.path == f"/public/v1/program/programs/{program_id}" + assert result.to_dict() == expected_response + + +def test_program_primitive_fields(program_data): + result = Program(program_data) + + assert result.to_dict() == program_data + + +def test_program_nested_fields_are_base_models(program_data): + result = Program(program_data) + + assert isinstance(result.vendor, BaseModel) + assert isinstance(result.eligibility, BaseModel) + assert isinstance(result.settings, BaseModel) + assert isinstance(result.statistics, BaseModel) + assert isinstance(result.audit, BaseModel) + + +def test_program_optional_fields(): + result = Program({"id": "PRG-123"}) + + assert result.id == "PRG-123" + assert not hasattr(result, "name") + assert not hasattr(result, "website") + assert not hasattr(result, "status") + assert not hasattr(result, "applicable_to") + assert not hasattr(result, "icon") + assert not hasattr(result, "vendor") + assert not hasattr(result, "settings") + assert not hasattr(result, "statistics") + assert not hasattr(result, "audit") diff --git a/tests/unit/test_mpt_client.py b/tests/unit/test_mpt_client.py index 2f788f0c..161f437e 100644 --- a/tests/unit/test_mpt_client.py +++ b/tests/unit/test_mpt_client.py @@ -13,6 +13,7 @@ AsyncHelpdesk, AsyncIntegration, AsyncNotifications, + AsyncProgram, Audit, Billing, Catalog, @@ -21,6 +22,7 @@ Helpdesk, Integration, Notifications, + Program, ) from tests.unit.conftest import API_TOKEN, API_URL @@ -41,6 +43,7 @@ def get_mpt_client(): ("helpdesk", Helpdesk), ("exchange", Exchange), ("integration", Integration), + ("program", Program), ], ) def test_mpt_client(resource_name: str, expected_type: type) -> None: @@ -74,6 +77,7 @@ def test_mpt_client_env(monkeypatch: pytest.MonkeyPatch) -> None: ("helpdesk", AsyncHelpdesk), ("exchange", AsyncExchange), ("integration", AsyncIntegration), + ("program", AsyncProgram), ], ) def test_async_mpt_client(resource_name: str, expected_type: type) -> None: