diff --git a/e2e_config.test.json b/e2e_config.test.json index 32cf582e..1e037c5a 100644 --- a/e2e_config.test.json +++ b/e2e_config.test.json @@ -70,8 +70,10 @@ "notifications.subscriber.id": "NTS-0829-7123-7123", "integration.extension.id": "EXT-6587-4477", "integration.term.id": "ETC-6587-4477-0062", + "program.document.file.id": "PDM-9643-3741-0001", "program.media.id": "PMD-9643-3741-0001", "program.parameter.group.id": "PPG-9643-3741-0002", + "program.parameter.id": "PPM-9643-3741-0001", "program.program.id": "PRG-9643-3741", - "program.document.file.id": "PDM-9643-3741-0001" + "program.template.id": "PTM-9643-3741-0004" } diff --git a/mpt_api_client/resources/program/programs.py b/mpt_api_client/resources/program/programs.py index 3066a853..df1caad6 100644 --- a/mpt_api_client/resources/program/programs.py +++ b/mpt_api_client/resources/program/programs.py @@ -22,6 +22,14 @@ AsyncParameterGroupsService, ParameterGroupsService, ) +from mpt_api_client.resources.program.programs_parameters import ( + AsyncParametersService, + ParametersService, +) +from mpt_api_client.resources.program.programs_templates import ( + AsyncTemplatesService, + TemplatesService, +) class Program(Model): @@ -101,6 +109,18 @@ def parameter_groups(self, program_id: str) -> ParameterGroupsService: http_client=self.http_client, endpoint_params={"program_id": program_id} ) + def parameters(self, program_id: str) -> ParametersService: # noqa: WPS110 + """Return program parameters service.""" + return ParametersService( + http_client=self.http_client, endpoint_params={"program_id": program_id} + ) + + def templates(self, program_id: str) -> TemplatesService: + """Return program templates service.""" + return TemplatesService( + http_client=self.http_client, endpoint_params={"program_id": program_id} + ) + class AsyncProgramsService( AsyncGetMixin[Program], @@ -140,3 +160,15 @@ def parameter_groups(self, program_id: str) -> AsyncParameterGroupsService: return AsyncParameterGroupsService( http_client=self.http_client, endpoint_params={"program_id": program_id} ) + + def parameters(self, program_id: str) -> AsyncParametersService: # noqa: WPS110 + """Return async program parameters service.""" + return AsyncParametersService( + http_client=self.http_client, endpoint_params={"program_id": program_id} + ) + + def templates(self, program_id: str) -> AsyncTemplatesService: + """Return async program templates service.""" + return AsyncTemplatesService( + http_client=self.http_client, endpoint_params={"program_id": program_id} + ) diff --git a/mpt_api_client/resources/program/programs_parameters.py b/mpt_api_client/resources/program/programs_parameters.py new file mode 100644 index 00000000..56fa8c6b --- /dev/null +++ b/mpt_api_client/resources/program/programs_parameters.py @@ -0,0 +1,69 @@ +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 + + +class Parameter(Model): + """Parameter resource. + + Attributes: + name: Parameter name. + scope: Parameter scope. + phase: Parameter phase (e.g. Order, Fulfillment). + program: Reference to the program this parameter belongs to. + description: Parameter description. + multiple: Whether multiple values are allowed for this parameter. + external_id: External identifier for the parameter. + display_order: Display order of the parameter. + constraints: Parameter constraints (required, hidden, readonly). + options: Type-specific parameter options. + type: Parameter type (e.g. SingleLineText, MultiLineText, Address, etc.). + status: Parameter status. + audit: Audit information (created, updated events). + """ + + name: str | None + scope: str | None + phase: str | None + program: BaseModel | None + description: str | None + multiple: bool | None + external_id: str | None + display_order: int | None + constraints: BaseModel | None + options: BaseModel | None + type: str | None + status: str | None + audit: BaseModel | None + + +class ParametersServiceConfig: + """Parameters service configuration.""" + + _endpoint = "/public/v1/program/programs/{program_id}/parameters" + _model_class = Parameter + _collection_key = "data" + + +class ParametersService( + ManagedResourceMixin[Parameter], + CollectionMixin[Parameter], + Service[Parameter], + ParametersServiceConfig, +): + """Parameters service.""" + + +class AsyncParametersService( + AsyncManagedResourceMixin[Parameter], + AsyncCollectionMixin[Parameter], + AsyncService[Parameter], + ParametersServiceConfig, +): + """Parameters service.""" diff --git a/mpt_api_client/resources/program/programs_templates.py b/mpt_api_client/resources/program/programs_templates.py new file mode 100644 index 00000000..10f12af1 --- /dev/null +++ b/mpt_api_client/resources/program/programs_templates.py @@ -0,0 +1,59 @@ +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 + + +class Template(Model): + """Template resource. + + Attributes: + name: Template name. + content: Template content. + default: Whether this is the default template. + external_ids: External identifiers. + type: Template type. + status: Template status. + program: Reference to the program. + audit: Audit information (created, updated events). + """ + + name: str | None + content: str | None # noqa: WPS110 + default: bool | None + external_ids: BaseModel | None + type: str | None + status: str | None + program: BaseModel | None + audit: BaseModel | None + + +class TemplatesServiceConfig: + """Templates service configuration.""" + + _endpoint = "/public/v1/program/programs/{program_id}/templates" + _model_class = Template + _collection_key = "data" + + +class TemplatesService( + ManagedResourceMixin[Template], + CollectionMixin[Template], + Service[Template], + TemplatesServiceConfig, +): + """Templates service.""" + + +class AsyncTemplatesService( + AsyncManagedResourceMixin[Template], + AsyncCollectionMixin[Template], + AsyncService[Template], + TemplatesServiceConfig, +): + """Templates service.""" diff --git a/pyproject.toml b/pyproject.toml index 58abc7dd..d6ed71af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,6 +125,7 @@ per-file-ignores = [ "mpt_api_client/resources/exchange/*.py: WPS235 WPS215", "mpt_api_client/resources/integration/*.py: WPS214 WPS215 WPS235", "mpt_api_client/resources/helpdesk/*.py: WPS204 WPS215 WPS214", + "mpt_api_client/resources/program/*.py: WPS204 WPS215", "mpt_api_client/rql/query_builder.py: WPS110 WPS115 WPS210 WPS214", "tests/e2e/accounts/*.py: WPS430 WPS202", "tests/e2e/billing/*.py: WPS202 WPS421 WPS118", diff --git a/tests/e2e/program/program/parameter/conftest.py b/tests/e2e/program/program/parameter/conftest.py new file mode 100644 index 00000000..e9592846 --- /dev/null +++ b/tests/e2e/program/program/parameter/conftest.py @@ -0,0 +1,32 @@ +import pytest + + +@pytest.fixture +def parameter_id(e2e_config): + return e2e_config["program.parameter.id"] + + +@pytest.fixture +def invalid_parameter_id(): + return "PPM-0000-0000-0000" + + +@pytest.fixture +def parameter_data(): + return { + "externalId": "e2eCreatedProgramParameter", + "displayOrder": 100, + "scope": "Enrollment", + "phase": "Fulfillment", + "multiple": False, + "description": "E2E Created Program Parameter", + "type": "SingleLineText", + "constraints": {"hidden": False, "required": False, "readonly": False}, + "name": "E2E Created Program Parameter", + "options": { + "type": "SingleLineText", + "placeholderText": "E2E Created Program Parameter", + "name": "E2E Created Program Parameter", + "hintText": "E2E Created Program Parameter", + }, + } diff --git a/tests/e2e/program/program/parameter/test_async_parameter.py b/tests/e2e/program/program/parameter/test_async_parameter.py new file mode 100644 index 00000000..cf636a34 --- /dev/null +++ b/tests/e2e/program/program/parameter/test_async_parameter.py @@ -0,0 +1,68 @@ +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_parameter(async_mpt_vendor, program_id, parameter_data): + service = async_mpt_vendor.program.programs.parameters(program_id) + parameter = await service.create(parameter_data) + yield parameter + try: + await service.delete(parameter.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete parameter {parameter.id}: {error.title}") # noqa: WPS421 + + +def test_create_parameter(created_parameter): + result = created_parameter.name == "E2E Created Program Parameter" + + assert result is True + + +async def test_update_parameter(async_mpt_vendor, program_id, created_parameter): + service = async_mpt_vendor.program.programs.parameters(program_id) + update_data = {"name": "E2E Updated Program Parameter"} + + result = await service.update(created_parameter.id, update_data) + + assert result.name == update_data["name"] + + +async def test_get_parameter(async_mpt_vendor, program_id, parameter_id): + service = async_mpt_vendor.program.programs.parameters(program_id) + + result = await service.get(parameter_id) + + assert result.id == parameter_id + + +async def test_get_invalid_parameter(async_mpt_vendor, program_id, invalid_parameter_id): + with pytest.raises(MPTAPIError): + await async_mpt_vendor.program.programs.parameters(program_id).get(invalid_parameter_id) + + +async def test_delete_parameter(async_mpt_vendor, program_id, created_parameter): + parameter_data = created_parameter + + result = async_mpt_vendor.program.programs.parameters(program_id) + + await result.delete(parameter_data.id) + + +async def test_filter_and_select_parameters(async_mpt_vendor, program_id, parameter_id): + select_fields = ["-description", "-audit"] + filtered_parameters = ( + async_mpt_vendor.program.programs + .parameters(program_id) + .filter(RQLQuery(id=parameter_id)) + .filter(RQLQuery(name="E2E Seeded Program Parameter")) + .select(*select_fields) + ) + + result = [parameter async for parameter in filtered_parameters.iterate()] + + assert len(result) == 1 diff --git a/tests/e2e/program/program/parameter/test_sync_parameter.py b/tests/e2e/program/program/parameter/test_sync_parameter.py new file mode 100644 index 00000000..00a31408 --- /dev/null +++ b/tests/e2e/program/program/parameter/test_sync_parameter.py @@ -0,0 +1,68 @@ +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_parameter(mpt_vendor, program_id, parameter_data): + service = mpt_vendor.program.programs.parameters(program_id) + parameter = service.create(parameter_data) + yield parameter + try: + service.delete(parameter.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete parameter {parameter.id}: {error.title}") # noqa: WPS421 + + +def test_create_parameter(created_parameter): + result = created_parameter.name == "E2E Created Program Parameter" + + assert result is True + + +def test_update_parameter(mpt_vendor, program_id, created_parameter): + service = mpt_vendor.program.programs.parameters(program_id) + update_data = {"name": "E2E Updated Program Parameter"} + + result = service.update(created_parameter.id, update_data) + + assert result.name == update_data["name"] + + +def test_get_parameter(mpt_vendor, program_id, parameter_id): + service = mpt_vendor.program.programs.parameters(program_id) + + result = service.get(parameter_id) + + assert result.id == parameter_id + + +def test_get_invalid_parameter(mpt_vendor, program_id, invalid_parameter_id): + with pytest.raises(MPTAPIError): + mpt_vendor.program.programs.parameters(program_id).get(invalid_parameter_id) + + +def test_delete_parameter(mpt_vendor, program_id, created_parameter): + parameter_data = created_parameter + + result = mpt_vendor.program.programs.parameters(program_id) + + result.delete(parameter_data.id) + + +def test_filter_and_select_parameters(mpt_vendor, program_id, parameter_id): + select_fields = ["-description", "-audit"] + filtered_parameters = ( + mpt_vendor.program.programs + .parameters(program_id) + .filter(RQLQuery(id=parameter_id)) + .filter(RQLQuery(name="E2E Seeded Program Parameter")) + .select(*select_fields) + ) + + result = list(filtered_parameters.iterate()) + + assert len(result) == 1 diff --git a/tests/e2e/program/program/template/conftest.py b/tests/e2e/program/program/template/conftest.py new file mode 100644 index 00000000..fc195707 --- /dev/null +++ b/tests/e2e/program/program/template/conftest.py @@ -0,0 +1,22 @@ +import pytest + + +@pytest.fixture +def template_id(e2e_config): + return e2e_config["program.template.id"] + + +@pytest.fixture +def invalid_template_id(): + return "PTM-0000-0000-0000" + + +@pytest.fixture +def template_data(): + return { + "name": "E2E Created Program Template", + "type": "EnrollmentProcessing", + "default": False, + "externalIds": {"vendor": None}, + "content": "E2E Created Program Template", + } diff --git a/tests/e2e/program/program/template/test_async_template.py b/tests/e2e/program/program/template/test_async_template.py new file mode 100644 index 00000000..f2db7460 --- /dev/null +++ b/tests/e2e/program/program/template/test_async_template.py @@ -0,0 +1,68 @@ +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_template(async_mpt_vendor, program_id, template_data): + service = async_mpt_vendor.program.programs.templates(program_id) + template = await service.create(template_data) + yield template + try: + await service.delete(template.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete template {template.id}: {error.title}") # noqa: WPS421 + + +def test_create_template(created_template): + result = created_template.name == "E2E Created Program Template" + + assert result is True + + +async def test_update_template(async_mpt_vendor, program_id, created_template): + service = async_mpt_vendor.program.programs.templates(program_id) + update_data = {"name": "E2E Updated Program Template"} + + result = await service.update(created_template.id, update_data) + + assert result.name == update_data["name"] + + +async def test_get_template(async_mpt_vendor, program_id, template_id): + service = async_mpt_vendor.program.programs.templates(program_id) + + result = await service.get(template_id) + + assert result.id == template_id + + +async def test_get_template_invalid_id(async_mpt_vendor, program_id, invalid_template_id): + with pytest.raises(MPTAPIError): + await async_mpt_vendor.program.programs.templates(program_id).get(invalid_template_id) + + +async def test_delete_template(async_mpt_vendor, program_id, created_template): + template_data = created_template + + result = async_mpt_vendor.program.programs.templates(program_id) + + await result.delete(template_data.id) + + +async def test_filter_and_select_templates(async_mpt_vendor, program_id, template_id): + select_fields = ["-content", "-audit"] + filtered_templates = ( + async_mpt_vendor.program.programs + .templates(program_id) + .filter(RQLQuery(id=template_id)) + .filter(RQLQuery(name="E2E Seeded Program Template")) + .select(*select_fields) + ) + + result = [template async for template in filtered_templates.iterate()] + + assert len(result) == 1 diff --git a/tests/e2e/program/program/template/test_sync_template.py b/tests/e2e/program/program/template/test_sync_template.py new file mode 100644 index 00000000..2629a1b0 --- /dev/null +++ b/tests/e2e/program/program/template/test_sync_template.py @@ -0,0 +1,68 @@ +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_template(mpt_vendor, program_id, template_data): + service = mpt_vendor.program.programs.templates(program_id) + template = service.create(template_data) + yield template + try: + service.delete(template.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete template {template.id}: {error.title}") # noqa: WPS421 + + +def test_create_template(created_template): + result = created_template.name == "E2E Created Program Template" + + assert result is True + + +def test_update_template(mpt_vendor, program_id, created_template): + service = mpt_vendor.program.programs.templates(program_id) + update_data = {"name": "E2E Updated Program Template"} + + result = service.update(created_template.id, update_data) + + assert result.name == update_data["name"] + + +def test_get_template(mpt_vendor, program_id, template_id): + service = mpt_vendor.program.programs.templates(program_id) + + result = service.get(template_id) + + assert result.id == template_id + + +def test_get_template_invalid_id(mpt_vendor, program_id, invalid_template_id): + with pytest.raises(MPTAPIError): + mpt_vendor.program.programs.templates(program_id).get(invalid_template_id) + + +def test_delete_template(mpt_vendor, program_id, created_template): + template_data = created_template + + result = mpt_vendor.program.programs.templates(program_id) + + result.delete(template_data.id) + + +def test_filter_and_select_templates(mpt_vendor, program_id, template_id): + select_fields = ["-content", "-audit"] + filtered_templates = ( + mpt_vendor.program.programs + .templates(program_id) + .filter(RQLQuery(id=template_id)) + .filter(RQLQuery(name="E2E Seeded Program Template")) + .select(*select_fields) + ) + + result = list(filtered_templates.iterate()) + + assert len(result) == 1 diff --git a/tests/unit/resources/program/test_programs.py b/tests/unit/resources/program/test_programs.py index 0c68d7d0..0edfe1e2 100644 --- a/tests/unit/resources/program/test_programs.py +++ b/tests/unit/resources/program/test_programs.py @@ -16,6 +16,14 @@ AsyncParameterGroupsService, ParameterGroupsService, ) +from mpt_api_client.resources.program.programs_parameters import ( + AsyncParametersService, + ParametersService, +) +from mpt_api_client.resources.program.programs_templates import ( + AsyncTemplatesService, + TemplatesService, +) @pytest.fixture @@ -94,6 +102,8 @@ def test_async_mixins_present(async_programs_service, method): ("documents", DocumentService), ("media", MediaService), ("parameter_groups", ParameterGroupsService), + ("parameters", ParametersService), + ("templates", TemplatesService), ], ) def test_property_services(programs_service, service_method, expected_service_class): @@ -109,6 +119,8 @@ def test_property_services(programs_service, service_method, expected_service_cl ("documents", AsyncDocumentService), ("media", AsyncMediaService), ("parameter_groups", AsyncParameterGroupsService), + ("parameters", AsyncParametersService), + ("templates", AsyncTemplatesService), ], ) def test_async_property_services(async_programs_service, service_method, expected_service_class): diff --git a/tests/unit/resources/program/test_programs_parameters.py b/tests/unit/resources/program/test_programs_parameters.py new file mode 100644 index 00000000..68b04874 --- /dev/null +++ b/tests/unit/resources/program/test_programs_parameters.py @@ -0,0 +1,103 @@ +import pytest + +from mpt_api_client.models.model import BaseModel +from mpt_api_client.resources.program.programs_parameters import ( + AsyncParametersService, + Parameter, + ParametersService, +) + + +@pytest.fixture +def parameters_service(http_client): + return ParametersService(http_client=http_client, endpoint_params={"program_id": "PPM-001"}) + + +@pytest.fixture +def async_parameters_service(async_http_client): + return AsyncParametersService( + http_client=async_http_client, endpoint_params={"program_id": "PPM-001"} + ) + + +@pytest.fixture +def parameter_data(): + return { + "id": "PRM-001", + "name": "Program Parameter", + "scope": "Enrollment", + "phase": "Fulfillment", + "program": {"id": "PPM-001", "name": "My Program"}, + "description": "Program Parameter", + "multiple": False, + "externalId": "ext-001", + "displayOrder": 1, + "constraints": {"required": True, "hidden": False, "readonly": False}, + "options": {"placeholder": "Program Parameter"}, + "type": "SingleLineText", + "status": "Active", + "audit": { + "created": {"at": "2024-01-01T00:00:00Z"}, + "updated": {"at": "2024-01-02T00:00:00Z"}, + }, + } + + +def test_endpoint(parameters_service): + result = parameters_service.path == "/public/v1/program/programs/PPM-001/parameters" + + assert result is True + + +def test_async_endpoint(async_parameters_service): + result = async_parameters_service.path == "/public/v1/program/programs/PPM-001/parameters" + + assert result is True + + +@pytest.mark.parametrize("method", ["get", "create", "delete", "update", "iterate"]) +def test_methods_present(parameters_service, method): + result = hasattr(parameters_service, method) + + assert result is True + + +@pytest.mark.parametrize("method", ["get", "create", "delete", "update", "iterate"]) +def test_async_methods_present(async_parameters_service, method): + result = hasattr(async_parameters_service, method) + + assert result is True + + +def test_template_primitive_fields(parameter_data): + result = Parameter(parameter_data) + + assert result.to_dict() == parameter_data + + +def test_template_nested_field_types(parameter_data): + result = Parameter(parameter_data) + + assert isinstance(result.program, BaseModel) + assert isinstance(result.constraints, BaseModel) + assert isinstance(result.options, BaseModel) + assert isinstance(result.audit, BaseModel) + + +def test_template_optional_fields_absent(): + result = Parameter({"id": "PRM-001"}) + + assert result.id == "PRM-001" + assert not hasattr(result, "name") + assert not hasattr(result, "scope") + assert not hasattr(result, "phase") + assert not hasattr(result, "program") + assert not hasattr(result, "description") + assert not hasattr(result, "multiple") + assert not hasattr(result, "external_id") + assert not hasattr(result, "display_order") + assert not hasattr(result, "constraints") + assert not hasattr(result, "options") + assert not hasattr(result, "type") + assert not hasattr(result, "status") + assert not hasattr(result, "audit") diff --git a/tests/unit/resources/program/test_programs_templates.py b/tests/unit/resources/program/test_programs_templates.py new file mode 100644 index 00000000..af030371 --- /dev/null +++ b/tests/unit/resources/program/test_programs_templates.py @@ -0,0 +1,92 @@ +import pytest + +from mpt_api_client.models.model import BaseModel +from mpt_api_client.resources.program.programs_templates import ( + AsyncTemplatesService, + Template, + TemplatesService, +) + + +@pytest.fixture +def templates_service(http_client): + return TemplatesService(http_client=http_client, endpoint_params={"program_id": "PTM-001"}) + + +@pytest.fixture +def async_templates_service(async_http_client): + return AsyncTemplatesService( + http_client=async_http_client, endpoint_params={"program_id": "PTM-001"} + ) + + +@pytest.fixture +def template_data(): + return { + "id": "TPL-001", + "name": "Program Template", + "content": "
Template Content
", + "default": True, + "externalIds": {"vendor": "EXT-001"}, + "type": "Email", + "status": "Active", + "program": {"id": "PTM-001", "name": "Program"}, + "audit": { + "created": {"at": "2024-01-01T00:00:00Z"}, + "updated": {"at": "2024-01-02T00:00:00Z"}, + }, + } + + +def test_endpoint(templates_service): + result = templates_service.path == "/public/v1/program/programs/PTM-001/templates" + + assert result is True + + +def test_async_endpoint(async_templates_service): + result = async_templates_service.path == "/public/v1/program/programs/PTM-001/templates" + + assert result is True + + +@pytest.mark.parametrize("method", ["get", "create", "delete", "update", "iterate"]) +def test_methods_present(templates_service, method): + result = hasattr(templates_service, method) + + assert result is True + + +@pytest.mark.parametrize("method", ["get", "create", "delete", "update", "iterate"]) +def test_async_methods_present(async_templates_service, method): + result = hasattr(async_templates_service, method) + + assert result is True + + +def test_template_primitive_fields(template_data): + result = Template(template_data) + + assert result.to_dict() == template_data + + +def test_template_nested_field_types(template_data): + result = Template(template_data) + + assert isinstance(result.external_ids, BaseModel) + assert isinstance(result.program, BaseModel) + assert isinstance(result.audit, BaseModel) + + +def test_template_optional_fields_absent(): + result = Template({"id": "TPL-001"}) + + assert result.id == "TPL-001" + assert not hasattr(result, "name") + assert not hasattr(result, "content") + assert not hasattr(result, "default") + assert not hasattr(result, "external_ids") + assert not hasattr(result, "type") + assert not hasattr(result, "status") + assert not hasattr(result, "program") + assert not hasattr(result, "audit")