diff --git a/e2e_config.test.json b/e2e_config.test.json index 55aadfb6..ec8528a7 100644 --- a/e2e_config.test.json +++ b/e2e_config.test.json @@ -71,6 +71,11 @@ "integration.extension.id": "EXT-6587-4477", "integration.term.id": "ETC-6587-4477-0062", "program.document.file.id": "PDM-9643-3741-0001", + "program.enrollment.assignee.id": "USR-6337-1324", + "program.enrollment.id": "ENR-3965-5056-7966", + "program.enrollment.process.template.id": "PTM-9643-3741-0001", + "program.enrollment.query.template.id": "PTM-9643-3741-0002", + "program.enrollment.complete.template.id": "PTM-9643-3741-0003", "program.media.id": "PMD-9643-3741-0001", "program.parameter.group.id": "PPG-9643-3741-0002", "program.parameter.id": "PPM-9643-3741-0001", diff --git a/mpt_api_client/resources/program/enrollments.py b/mpt_api_client/resources/program/enrollments.py new file mode 100644 index 00000000..30644657 --- /dev/null +++ b/mpt_api_client/resources/program/enrollments.py @@ -0,0 +1,224 @@ +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, ResourceData +from mpt_api_client.resources.program.mixins.render_mixin import AsyncRenderMixin, RenderMixin + + +class Enrollment(Model): + """Program enrollment resource. + + Attributes: + name: Enrollment name. + certificate: Reference to the certificate. + program: Reference to the program. + vendor: Reference to the vendor. + applicable_to: Applicable to which entities. + type: Enrollment type. + licensee: Reference to the licensee. + eligibility: Eligibility criteria. + status: Enrollment status. + parameters: Enrollment parameters. + template: Reference to the enrollment template. + audit: Audit information. + """ + + name: str | None + certificate: BaseModel | None + program: BaseModel | None + vendor: BaseModel | None + applicable_to: str | None + type: str | None + licensee: BaseModel | None + eligibility: BaseModel | None + status: str | None + parameters: BaseModel | None # noqa: WPS110 + template: BaseModel | None + audit: BaseModel | None + + +class EnrollmentServiceConfig: + """Program enrollment service config.""" + + _endpoint = "/public/v1/program/enrollments" + _model_class = Enrollment + _collection_key = "data" + + +class EnrollmentService( + RenderMixin[Enrollment], + ManagedResourceMixin[Enrollment], + CollectionMixin[Enrollment], + Service[Enrollment], + EnrollmentServiceConfig, +): + """Program enrollment service.""" + + def validate(self, resource_id: str, resource_data: ResourceData | None = None) -> Enrollment: + """Validate enrollment. + + Args: + resource_id: Enrollment ID + resource_data: Enrollment data will be validated + + Returns: + Validated enrollment. + """ + return self._resource(resource_id).post("validate", json=resource_data) + + def query(self, resource_id: str, resource_data: ResourceData | None = None) -> Enrollment: + """Query enrollment. + + Args: + resource_id: Enrollment ID + resource_data: Enrollment data will be queried + + Returns: + Queried enrollment. + """ + return self._resource(resource_id).post("query", json=resource_data) + + def process(self, resource_id: str, resource_data: ResourceData | None = None) -> Enrollment: + """Process enrollment. + + Args: + resource_id: Enrollment ID + resource_data: Enrollment data will be processed + + Returns: + Processed enrollment. + """ + return self._resource(resource_id).post("process", json=resource_data) + + def complete(self, resource_id: str, resource_data: ResourceData | None = None) -> Enrollment: + """Complete enrollment. + + Args: + resource_id: Enrollment ID + resource_data: Enrollment data will be completed + + Returns: + Completed enrollment. + """ + return self._resource(resource_id).post("complete", json=resource_data) + + def submit(self, resource_id: str, resource_data: ResourceData | None = None) -> Enrollment: + """Submit enrollment. + + Args: + resource_id: Enrollment ID + resource_data: Enrollment data will be submitted + + Returns: + Submitted enrollment. + """ + return self._resource(resource_id).post("submit", json=resource_data) + + def fail(self, resource_id: str, resource_data: ResourceData | None = None) -> Enrollment: + """Fail enrollment. + + Args: + resource_id: Enrollment ID + resource_data: Enrollment data will be failed + + Returns: + Failed enrollment. + """ + return self._resource(resource_id).post("fail", json=resource_data) + + +class AsyncEnrollmentService( + AsyncRenderMixin[Enrollment], + AsyncManagedResourceMixin[Enrollment], + AsyncCollectionMixin[Enrollment], + AsyncService[Enrollment], + EnrollmentServiceConfig, +): + """Async program enrollment service.""" + + async def validate( + self, resource_id: str, resource_data: ResourceData | None = None + ) -> Enrollment: + """Validate enrollment. + + Args: + resource_id: Enrollment ID + resource_data: Enrollment data will be validated + + Returns: + Validated enrollment. + """ + return await self._resource(resource_id).post("validate", json=resource_data) + + async def query( + self, resource_id: str, resource_data: ResourceData | None = None + ) -> Enrollment: + """Query enrollment. + + Args: + resource_id: Enrollment ID + resource_data: Enrollment data will be queried + + Returns: + Queried enrollment. + """ + return await self._resource(resource_id).post("query", json=resource_data) + + async def process( + self, resource_id: str, resource_data: ResourceData | None = None + ) -> Enrollment: + """Process enrollment. + + Args: + resource_id: Enrollment ID + resource_data: Enrollment data will be processed + + Returns: + Processed enrollment. + """ + return await self._resource(resource_id).post("process", json=resource_data) + + async def complete( + self, resource_id: str, resource_data: ResourceData | None = None + ) -> Enrollment: + """Complete enrollment. + + Args: + resource_id: Enrollment ID + resource_data: Enrollment data will be completed + + Returns: + Completed enrollment. + """ + return await self._resource(resource_id).post("complete", json=resource_data) + + async def submit( + self, resource_id: str, resource_data: ResourceData | None = None + ) -> Enrollment: + """Submit enrollment. + + Args: + resource_id: Enrollment ID + resource_data: Enrollment data will be submitted + + Returns: + Submitted enrollment. + """ + return await self._resource(resource_id).post("submit", json=resource_data) + + async def fail(self, resource_id: str, resource_data: ResourceData | None = None) -> Enrollment: + """Fail enrollment. + + Args: + resource_id: Enrollment ID + resource_data: Enrollment data will be failed + + Returns: + Failed enrollment. + """ + return await self._resource(resource_id).post("fail", json=resource_data) diff --git a/mpt_api_client/resources/program/mixins/render_mixin.py b/mpt_api_client/resources/program/mixins/render_mixin.py new file mode 100644 index 00000000..83c94750 --- /dev/null +++ b/mpt_api_client/resources/program/mixins/render_mixin.py @@ -0,0 +1,30 @@ +class RenderMixin[Model]: + """Render resource mixin.""" + + def render(self, resource_id: str) -> str: + """Render resource. + + Args: + resource_id: Resource ID + + Returns: + Rendered resource. + """ + response = self._resource(resource_id).do_request("GET", "render") # type: ignore[attr-defined] + return response.text # type: ignore[no-any-return] + + +class AsyncRenderMixin[Model]: + """Asynchronous render resource mixin.""" + + async def render(self, resource_id: str) -> str: + """Render resource. + + Args: + resource_id: Resource ID + + Returns: + Rendered resource. + """ + response = await self._resource(resource_id).do_request("GET", "render") # type: ignore[attr-defined] + return response.text # type: ignore[no-any-return] diff --git a/mpt_api_client/resources/program/program.py b/mpt_api_client/resources/program/program.py index 2b31cba3..aae30ad5 100644 --- a/mpt_api_client/resources/program/program.py +++ b/mpt_api_client/resources/program/program.py @@ -1,4 +1,5 @@ from mpt_api_client.http import AsyncHTTPClient, HTTPClient +from mpt_api_client.resources.program.enrollments import AsyncEnrollmentService, EnrollmentService from mpt_api_client.resources.program.programs import AsyncProgramsService, ProgramsService @@ -13,6 +14,11 @@ def programs(self) -> ProgramsService: """Programs service.""" return ProgramsService(http_client=self.http_client) + @property + def enrollments(self) -> EnrollmentService: + """Enrollments service.""" + return EnrollmentService(http_client=self.http_client) + class AsyncProgram: """Program MPT API Module.""" @@ -24,3 +30,8 @@ def __init__(self, *, http_client: AsyncHTTPClient): def programs(self) -> AsyncProgramsService: """Programs service.""" return AsyncProgramsService(http_client=self.http_client) + + @property + def enrollments(self) -> AsyncEnrollmentService: + """Enrollments service.""" + return AsyncEnrollmentService(http_client=self.http_client) diff --git a/pyproject.toml b/pyproject.toml index 029608e2..bbafa37a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -139,6 +139,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/enrollment/*.py: WPS202 WPS204", "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", diff --git a/tests/e2e/program/enrollment/conftest.py b/tests/e2e/program/enrollment/conftest.py new file mode 100644 index 00000000..203d4dd8 --- /dev/null +++ b/tests/e2e/program/enrollment/conftest.py @@ -0,0 +1,79 @@ +import pytest + + +@pytest.fixture +def enrollment_id(e2e_config): + return e2e_config["program.enrollment.id"] + + +@pytest.fixture +def invalid_enrollment_id(): + return "ENR-0000-0000-0000" + + +@pytest.fixture +def query_template_id(e2e_config): + return e2e_config["program.enrollment.query.template.id"] + + +@pytest.fixture +def process_template_id(e2e_config): + return e2e_config["program.enrollment.process.template.id"] + + +@pytest.fixture +def complete_template_id(e2e_config): + return e2e_config["program.enrollment.complete.template.id"] + + +@pytest.fixture +def assignee_id(e2e_config): + return e2e_config["program.enrollment.assignee.id"] + + +@pytest.fixture +def enrollment_data(program_id, licensee_id): + return { + "program": {"id": program_id}, + "parameters": {"ordering": []}, + "certificant": {"id": licensee_id}, + "licensee": {"id": licensee_id}, + } + + +@pytest.fixture +def status_flow_enrollment_data_factory(): + def factory(enrollment_id: str, template_id: str): + return { + "id": enrollment_id, + "template": { + "id": template_id, + "content": "TEMPLATE_CONTENT", + }, + } + + return factory + + +@pytest.fixture +def enrollment_status_message_factory(): + def factory(enrollment_id: str): + return { + "id": enrollment_id, + "statusNotes": { + "message": "Failing enrollment for E2E test", + }, + } + + return factory + + +@pytest.fixture +def assignee_enrollment_data_factory(assignee_id): + def factory(enrollment_id: str): + return { + "id": enrollment_id, + "assignee": {"id": assignee_id}, + } + + return factory diff --git a/tests/e2e/program/enrollment/test_async_enrollment.py b/tests/e2e/program/enrollment/test_async_enrollment.py new file mode 100644 index 00000000..4431bc54 --- /dev/null +++ b/tests/e2e/program/enrollment/test_async_enrollment.py @@ -0,0 +1,162 @@ +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_enrollment(async_mpt_client, enrollment_data): + enrollment = await async_mpt_client.program.enrollments.create(enrollment_data) + + yield enrollment + + try: + await async_mpt_client.program.enrollments.delete(enrollment.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete enrollment {enrollment.id}: {error.title}") # noqa: WPS421 + + +@pytest.fixture +async def submitted_enrollment(created_enrollment, async_mpt_client): + return await async_mpt_client.program.enrollments.submit(created_enrollment.id) + + +@pytest.fixture +async def queried_enrollment( + submitted_enrollment, async_mpt_vendor, status_flow_enrollment_data_factory, query_template_id +): + query_enrollment_data = status_flow_enrollment_data_factory( + enrollment_id=submitted_enrollment.id, + template_id=query_template_id, + ) + return await async_mpt_vendor.program.enrollments.query( + submitted_enrollment.id, query_enrollment_data + ) + + +@pytest.fixture +async def processed_enrollment( + async_mpt_vendor, queried_enrollment, status_flow_enrollment_data_factory, process_template_id +): + process_enrollment_data = status_flow_enrollment_data_factory( + enrollment_id=queried_enrollment.id, + template_id=process_template_id, + ) + return await async_mpt_vendor.program.enrollments.process( + queried_enrollment.id, process_enrollment_data + ) + + +async def test_get_enrollment_by_id(async_mpt_client, enrollment_id): + result = await async_mpt_client.program.enrollments.get(enrollment_id) + + assert result is not None + + +async def test_list_enrollments(async_mpt_client): + limit = 10 + + result = await async_mpt_client.program.enrollments.fetch_page(limit=limit) + + assert len(result) > 0 + + +async def test_get_enrollment_by_id_not_found(async_mpt_client, invalid_enrollment_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await async_mpt_client.program.enrollments.get(invalid_enrollment_id) + + +async def test_filter_enrollments(async_mpt_client, enrollment_id): + filtered_enrollments = async_mpt_client.program.enrollments.filter( + RQLQuery(id=enrollment_id) + ).filter(RQLQuery(status="Completed")) + + result = [enrollment async for enrollment in filtered_enrollments.iterate()] + + assert len(result) == 1 + + +def test_create_enrollment(created_enrollment): + result = created_enrollment + + assert result is not None + + +async def test_delete_enrollment(async_mpt_client, created_enrollment): + enrollment_data = created_enrollment + + result = async_mpt_client.program.enrollments + + await result.delete(enrollment_data.id) + + +async def test_update_enrollment( + async_mpt_vendor, submitted_enrollment, assignee_enrollment_data_factory +): + update_data = assignee_enrollment_data_factory(submitted_enrollment.id) + + result = await async_mpt_vendor.program.enrollments.update(submitted_enrollment.id, update_data) + + assert result is not None + + +async def test_submit_enrollment(async_mpt_client, created_enrollment): + result = await async_mpt_client.program.enrollments.submit(created_enrollment.id) + + assert result is not None + + +async def test_validate_enrollment( + async_mpt_client, submitted_enrollment, enrollment_status_message_factory +): + status_message_data = enrollment_status_message_factory(submitted_enrollment.id) + + result = await async_mpt_client.program.enrollments.validate( + submitted_enrollment.id, status_message_data + ) + + assert result is not None + + +def test_query_enrollment(queried_enrollment): + result = queried_enrollment + + assert result is not None + + +def test_process_enrollment(processed_enrollment): + result = processed_enrollment + + assert result is not None + + +async def test_fail_enrollment( + async_mpt_vendor, queried_enrollment, enrollment_status_message_factory +): + status_message_data = enrollment_status_message_factory(queried_enrollment.id) + + result = await async_mpt_vendor.program.enrollments.fail( + queried_enrollment.id, status_message_data + ) + + assert result is not None + + +async def test_complete_enrollment( + async_mpt_vendor, + submitted_enrollment, + status_flow_enrollment_data_factory, + complete_template_id, +): + complete_enrollment_data = status_flow_enrollment_data_factory( + enrollment_id=submitted_enrollment.id, + template_id=complete_template_id, + ) + + result = await async_mpt_vendor.program.enrollments.complete( + submitted_enrollment.id, complete_enrollment_data + ) + + assert result is not None diff --git a/tests/e2e/program/enrollment/test_sync_enrollment.py b/tests/e2e/program/enrollment/test_sync_enrollment.py new file mode 100644 index 00000000..4b67e73f --- /dev/null +++ b/tests/e2e/program/enrollment/test_sync_enrollment.py @@ -0,0 +1,145 @@ +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_enrollment(mpt_client, enrollment_data): + enrollment = mpt_client.program.enrollments.create(enrollment_data) + + yield enrollment + + try: + mpt_client.program.enrollments.delete(enrollment.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete enrollment {enrollment.id}: {error.title}") # noqa: WPS421 + + +@pytest.fixture +def submitted_enrollment(created_enrollment, mpt_client): + return mpt_client.program.enrollments.submit(created_enrollment.id) + + +@pytest.fixture +def queried_enrollment( + submitted_enrollment, mpt_vendor, status_flow_enrollment_data_factory, query_template_id +): + query_enrollment_data = status_flow_enrollment_data_factory( + enrollment_id=submitted_enrollment.id, + template_id=query_template_id, + ) + return mpt_vendor.program.enrollments.query(submitted_enrollment.id, query_enrollment_data) + + +@pytest.fixture +def processed_enrollment( + mpt_vendor, queried_enrollment, status_flow_enrollment_data_factory, process_template_id +): + process_enrollment_data = status_flow_enrollment_data_factory( + enrollment_id=queried_enrollment.id, + template_id=process_template_id, + ) + return mpt_vendor.program.enrollments.process(queried_enrollment.id, process_enrollment_data) + + +def test_get_enrollment_by_id(mpt_client, enrollment_id): + result = mpt_client.program.enrollments.get(enrollment_id) + + assert result is not None + + +def test_list_enrollments(mpt_client): + limit = 10 + + result = mpt_client.program.enrollments.fetch_page(limit=limit) + + assert len(result) > 0 + + +def test_get_enrollment_by_id_not_found(mpt_client, invalid_enrollment_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + mpt_client.program.enrollments.get(invalid_enrollment_id) + + +def test_filter_enrollments(mpt_client, enrollment_id): + filtered_enrollments = mpt_client.program.enrollments.filter(RQLQuery(id=enrollment_id)).filter( + RQLQuery(status="Completed") + ) + + result = list(filtered_enrollments.iterate()) + + assert len(result) == 1 + + +def test_create_enrollment(created_enrollment): + result = created_enrollment + + assert result is not None + + +def test_delete_enrollment(mpt_client, created_enrollment): + enrollment_data = created_enrollment + + result = mpt_client.program.enrollments + + result.delete(enrollment_data.id) + + +def test_update_enrollment(mpt_vendor, submitted_enrollment, assignee_enrollment_data_factory): + update_data = assignee_enrollment_data_factory(submitted_enrollment.id) + + result = mpt_vendor.program.enrollments.update(submitted_enrollment.id, update_data) + + assert result is not None + + +def test_submit_enrollment(mpt_client, created_enrollment): + result = mpt_client.program.enrollments.submit(created_enrollment.id) + + assert result is not None + + +def test_validate_enrollment(mpt_client, submitted_enrollment, enrollment_status_message_factory): + status_message_data = enrollment_status_message_factory(submitted_enrollment.id) + + result = mpt_client.program.enrollments.validate(submitted_enrollment.id, status_message_data) + + assert result is not None + + +def test_query_enrollment(queried_enrollment): + result = queried_enrollment + + assert result is not None + + +def test_process_enrollment(processed_enrollment): + result = processed_enrollment + + assert result is not None + + +def test_fail_enrollment(mpt_vendor, queried_enrollment, enrollment_status_message_factory): + status_message_data = enrollment_status_message_factory(queried_enrollment.id) + + result = mpt_vendor.program.enrollments.fail(queried_enrollment.id, status_message_data) + + assert result is not None + + +def test_complete_enrollment( + mpt_vendor, submitted_enrollment, status_flow_enrollment_data_factory, complete_template_id +): + complete_enrollment_data = status_flow_enrollment_data_factory( + enrollment_id=submitted_enrollment.id, + template_id=complete_template_id, + ) + + result = mpt_vendor.program.enrollments.complete( + submitted_enrollment.id, complete_enrollment_data + ) + + assert result is not None diff --git a/tests/unit/resources/program/mixin/test_render_mixin.py b/tests/unit/resources/program/mixin/test_render_mixin.py new file mode 100644 index 00000000..f5aa86f7 --- /dev/null +++ b/tests/unit/resources/program/mixin/test_render_mixin.py @@ -0,0 +1,56 @@ +import httpx +import respx + +from mpt_api_client.http import AsyncService, Service +from mpt_api_client.resources.program.mixins.render_mixin import AsyncRenderMixin, RenderMixin +from tests.unit.conftest import DummyModel + + +class DummyRenderService( + RenderMixin[DummyModel], + Service[DummyModel], +): + _endpoint = "public/v1/dummy/render" + _model_class = DummyModel + + +class AsyncDummyRenderService( + AsyncRenderMixin[DummyModel], + AsyncService[DummyModel], +): + _endpoint = "public/v1/dummy/render" + _model_class = DummyModel + + +def test_render(http_client): + service = DummyRenderService(http_client=http_client) + rendered_content = "

Dummy Rendered Content

" + with respx.mock: + respx.get("https://api.example.com/public/v1/dummy/render/DUMMY-123/render").mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "text/html"}, + content=rendered_content, + ) + ) + + result = service.render("DUMMY-123") + + assert result == rendered_content + + +async def test_async_render(async_http_client): + service = AsyncDummyRenderService(http_client=async_http_client) + rendered_content = "

Dummy Rendered Content

" + with respx.mock: + respx.get("https://api.example.com/public/v1/dummy/render/DUMMY-123/render").mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "text/html"}, + content=rendered_content, + ) + ) + + result = await service.render("DUMMY-123") + + assert result == rendered_content diff --git a/tests/unit/resources/program/test_enrollments.py b/tests/unit/resources/program/test_enrollments.py new file mode 100644 index 00000000..cfb159b9 --- /dev/null +++ b/tests/unit/resources/program/test_enrollments.py @@ -0,0 +1,213 @@ +import httpx +import pytest +import respx + +from mpt_api_client.models.model import BaseModel +from mpt_api_client.resources.program.enrollments import ( + AsyncEnrollmentService, + Enrollment, + EnrollmentService, +) + + +@pytest.fixture +def enrollment_service(http_client): + return EnrollmentService(http_client=http_client) + + +@pytest.fixture +def async_enrollment_service(async_http_client): + return AsyncEnrollmentService(http_client=async_http_client) + + +@pytest.fixture +def enrollment_data(): + return { + "id": "ENR-123", + "name": "Enrollment 123", + "status": "active", + "program": {"id": "PRG-123"}, + "certificate": {"id": "CRT-123"}, + "vendor": {"id": "ACC-123"}, + "applicableTo": "all", + "type": "standard", + "licensee": {"id": "LCE-123"}, + "eligibility": {"criteria": "must be eligible"}, + "parameters": {"param1": "value1"}, + "template": {"id": "TMP-123"}, + "audit": {"created": "2024-01-01T00:00:00Z", "updated": "2024-01-02T00:00:00Z"}, + } + + +@pytest.mark.parametrize( + ("action", "input_status"), + [ + ("validate", {"id": "ENR-123", "status": "updated"}), + ("query", {"id": "ENR-123", "status": "updated"}), + ("process", {"id": "ENR-123", "status": "updated"}), + ("complete", {"id": "ENR-123", "status": "updated"}), + ("submit", {"id": "ENR-123", "status": "updated"}), + ("fail", {"id": "ENR-123", "status": "updated"}), + ], +) +def test_custom_resource_actions(enrollment_service, action, input_status): + request_expected_content = b'{"id":"ENR-123","status":"updated"}' + response_expected_data = {"id": "ENR-123", "status": "new_status"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/program/enrollments/ENR-123/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + + result = getattr(enrollment_service, action)("ENR-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, Enrollment) + + +@pytest.mark.parametrize( + ("action", "input_status"), + [ + ("validate", None), + ("query", None), + ("process", None), + ("complete", None), + ("submit", None), + ("fail", None), + ], +) +def test_custom_resource_actions_no_data(enrollment_service, action, input_status): + request_expected_content = b"" + response_expected_data = {"id": "ENR-123", "status": "new_status"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/program/enrollments/ENR-123/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + + result = getattr(enrollment_service, action)("ENR-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, Enrollment) + + +@pytest.mark.parametrize( + ("action", "input_status"), + [ + ("validate", {"id": "ENR-123", "status": "updated"}), + ("query", {"id": "ENR-123", "status": "updated"}), + ("process", {"id": "ENR-123", "status": "updated"}), + ("complete", {"id": "ENR-123", "status": "updated"}), + ("submit", {"id": "ENR-123", "status": "updated"}), + ("fail", {"id": "ENR-123", "status": "updated"}), + ], +) +async def test_async_custom_resource_actions(async_enrollment_service, action, input_status): + request_expected_content = b'{"id":"ENR-123","status":"updated"}' + response_expected_data = {"id": "ENR-123", "status": "new_status"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/program/enrollments/ENR-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_enrollment_service, action)("ENR-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, Enrollment) + + +@pytest.mark.parametrize( + ("action", "input_status"), + [ + ("validate", None), + ("query", None), + ("process", None), + ("complete", None), + ("submit", None), + ("fail", None), + ], +) +async def test_async_custom_resource_actions_no_data( + async_enrollment_service, action, input_status +): + request_expected_content = b"" + response_expected_data = {"id": "ENR-123", "status": "new_status"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/program/enrollments/ENR-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_enrollment_service, action)("ENR-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, Enrollment) + + +def test_enrollment_primitive_fields(enrollment_data): + result = Enrollment(enrollment_data) + + assert result.to_dict() == enrollment_data + + +def test_enrollment_nested_fields(enrollment_data): + result = Enrollment(enrollment_data) + + assert isinstance(result.program, BaseModel) + assert isinstance(result.certificate, BaseModel) + assert isinstance(result.vendor, BaseModel) + assert isinstance(result.licensee, BaseModel) + assert isinstance(result.eligibility, BaseModel) + assert isinstance(result.parameters, BaseModel) + assert isinstance(result.template, BaseModel) + assert isinstance(result.audit, BaseModel) + + +def test_enrollment_optional_fields(): + result = Enrollment({}) + + assert not hasattr(result, "name") + assert not hasattr(result, "certificate") + assert not hasattr(result, "program") + assert not hasattr(result, "vendor") + assert not hasattr(result, "applicable_to") + assert not hasattr(result, "type") + assert not hasattr(result, "licensee") + assert not hasattr(result, "eligibility") + assert not hasattr(result, "status") + assert not hasattr(result, "parameters") + assert not hasattr(result, "template") + assert not hasattr(result, "audit") diff --git a/tests/unit/resources/program/test_program.py b/tests/unit/resources/program/test_program.py index 6e1a8d97..487d1a40 100644 --- a/tests/unit/resources/program/test_program.py +++ b/tests/unit/resources/program/test_program.py @@ -1,5 +1,6 @@ import pytest +from mpt_api_client.resources.program.enrollments import AsyncEnrollmentService, EnrollmentService from mpt_api_client.resources.program.program import AsyncProgram, Program from mpt_api_client.resources.program.programs import AsyncProgramsService, ProgramsService @@ -18,6 +19,7 @@ def async_program(async_http_client): ("property_name", "expected_service_class"), [ ("programs", ProgramsService), + ("enrollments", EnrollmentService), ], ) def test_program_properties(program, property_name, expected_service_class): @@ -31,6 +33,7 @@ def test_program_properties(program, property_name, expected_service_class): ("property_name", "expected_service_class"), [ ("programs", AsyncProgramsService), + ("enrollments", AsyncEnrollmentService), ], ) def test_async_program_properties(async_program, property_name, expected_service_class):