From 3474af2691e501a1827d4863a5644015f91d6f73 Mon Sep 17 00:00:00 2001 From: Albert Sola Date: Tue, 7 Apr 2026 11:24:57 +0100 Subject: [PATCH 1/3] MPT-19907: add /public/v1/integration/extensions/{extensionId}/terms/{termId}/variants endpoint Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../integration/extension_term_variants.py | 53 +++++- .../test_extension_term_variants.py | 174 ++++++++++++++++++ 2 files changed, 224 insertions(+), 3 deletions(-) create mode 100644 tests/unit/resources/integration/test_extension_term_variants.py diff --git a/mpt_api_client/resources/integration/extension_term_variants.py b/mpt_api_client/resources/integration/extension_term_variants.py index a8b8061a..364758f2 100644 --- a/mpt_api_client/resources/integration/extension_term_variants.py +++ b/mpt_api_client/resources/integration/extension_term_variants.py @@ -1,13 +1,52 @@ from mpt_api_client.http import AsyncService, Service from mpt_api_client.http.mixins import ( AsyncCollectionMixin, + AsyncCreateFileMixin, + AsyncModifiableResourceMixin, CollectionMixin, + CreateFileMixin, + ModifiableResourceMixin, ) from mpt_api_client.models import Model +from mpt_api_client.models.model import BaseModel +from mpt_api_client.resources.integration.mixins import ( + AsyncPublishableMixin, + PublishableMixin, +) class ExtensionTermVariant(Model): - """Extension Term Variant resource (stub).""" + """Extension Term Variant resource. + + Attributes: + name: Variant name. + revision: Revision number. + type: Variant type (Online or File). + asset_url: URL to the variant asset for Online type. + language_code: Language code for this variant. + description: Variant description. + status: Variant status (Draft, Published, Unpublished, Deleted). + filename: Original file name for File type. + size: File size in bytes for File type. + content_type: MIME content type of the file. + term: Reference to the parent term. + file_id: Identifier of the uploaded file. + audit: Audit information (created, updated, published, unpublished). + """ + + name: str | None + revision: int | None + type: str | None + asset_url: str | None + language_code: str | None + description: str | None + status: str | None + filename: str | None + size: int | None + content_type: str | None + term: BaseModel | None + file_id: str | None + audit: BaseModel | None class ExtensionTermVariantsServiceConfig: @@ -16,19 +55,27 @@ class ExtensionTermVariantsServiceConfig: _endpoint = "/public/v1/integration/extensions/{extension_id}/terms/{term_id}/variants" _model_class = ExtensionTermVariant _collection_key = "data" + _upload_file_key = "file" + _upload_data_key = "variant" class ExtensionTermVariantsService( + PublishableMixin[ExtensionTermVariant], + CreateFileMixin[ExtensionTermVariant], + ModifiableResourceMixin[ExtensionTermVariant], CollectionMixin[ExtensionTermVariant], Service[ExtensionTermVariant], ExtensionTermVariantsServiceConfig, ): - """Sync service for extension term variants (stub).""" + """Sync service for extensions/{extensionId}/terms/{termId}/variants endpoint.""" class AsyncExtensionTermVariantsService( + AsyncPublishableMixin[ExtensionTermVariant], + AsyncCreateFileMixin[ExtensionTermVariant], + AsyncModifiableResourceMixin[ExtensionTermVariant], AsyncCollectionMixin[ExtensionTermVariant], AsyncService[ExtensionTermVariant], ExtensionTermVariantsServiceConfig, ): - """Async service for extension term variants (stub).""" + """Async service for extensions/{extensionId}/terms/{termId}/variants endpoint.""" diff --git a/tests/unit/resources/integration/test_extension_term_variants.py b/tests/unit/resources/integration/test_extension_term_variants.py new file mode 100644 index 00000000..ea714c17 --- /dev/null +++ b/tests/unit/resources/integration/test_extension_term_variants.py @@ -0,0 +1,174 @@ +from typing import Any + +import httpx +import pytest +import respx + +from mpt_api_client.models.model import BaseModel +from mpt_api_client.resources.integration.extension_term_variants import ( + AsyncExtensionTermVariantsService, + ExtensionTermVariant, + ExtensionTermVariantsService, +) +from mpt_api_client.resources.integration.extension_terms import ExtensionTermsService + +BASE_URL = "https://api.example.com" +VARIANTS_URL = f"{BASE_URL}/public/v1/integration/extensions/EXT-001/terms/TERM-001/variants" + + +@pytest.fixture +def variants_service(http_client: Any) -> ExtensionTermVariantsService: + return ExtensionTermVariantsService( + http_client=http_client, + endpoint_params={"extension_id": "EXT-001", "term_id": "TERM-001"}, + ) + + +@pytest.fixture +def async_variants_service(async_http_client: Any) -> AsyncExtensionTermVariantsService: + return AsyncExtensionTermVariantsService( + http_client=async_http_client, + endpoint_params={"extension_id": "EXT-001", "term_id": "TERM-001"}, + ) + + +@pytest.fixture +def terms_service(http_client: Any) -> ExtensionTermsService: + return ExtensionTermsService( + http_client=http_client, endpoint_params={"extension_id": "EXT-001"} + ) + + +@pytest.fixture +def variant_data() -> dict: + return { + "id": "TRV-001", + "name": "English Variant", + "revision": 1, + "type": "File", + "assetUrl": None, + "languageCode": "en-US", + "description": "English language variant", + "status": "Draft", + "filename": "terms.pdf", + "size": 2048, + "contentType": "application/pdf", + "term": {"id": "TERM-001"}, + "fileId": "FILE-001", + "audit": {"created": {"at": "2024-01-01T00:00:00Z"}}, + } + + +@pytest.mark.parametrize( + "method", + ["get", "create", "update", "delete", "publish", "unpublish", "iterate"], +) +def test_mixins_present(variants_service: ExtensionTermVariantsService, method: str) -> None: + result = hasattr(variants_service, method) + + assert result is True + + +@pytest.mark.parametrize( + "method", + ["get", "create", "update", "delete", "publish", "unpublish", "iterate"], +) +def test_async_mixins_present( + async_variants_service: AsyncExtensionTermVariantsService, method: str +) -> None: + result = hasattr(async_variants_service, method) + + assert result is True + + +def test_extension_term_variant_primitive_fields(variant_data: dict) -> None: + result = ExtensionTermVariant(variant_data) + + assert result.to_dict() == variant_data + + +def test_extension_term_variant_nested_fields(variant_data: dict) -> None: + result = ExtensionTermVariant(variant_data) + + assert isinstance(result.term, BaseModel) + assert isinstance(result.audit, BaseModel) + + +def test_extension_term_variant_create( + variants_service: ExtensionTermVariantsService, tmp_path: Any +) -> None: + variant_payload = { + "Type": "File", + "Name": "English Variant", + "LanguageCode": "en-US", + "Description": "English language variant", + } + response_data = {"id": "TRV-001", "name": "English Variant", "status": "Draft"} + with respx.mock: + mock_route = respx.post(VARIANTS_URL).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_data, + ) + ) + file_path = tmp_path / "terms.pdf" + file_path.write_bytes(b"fake-pdf-content") + + with file_path.open("rb") as fp: + result = variants_service.create(variant_payload, fp) + + assert mock_route.call_count == 1 + assert result.to_dict() == response_data + + +def test_extension_term_variant_get( + variants_service: ExtensionTermVariantsService, variant_data: dict +) -> None: + with respx.mock: + respx.get(f"{VARIANTS_URL}/TRV-001").mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + json=variant_data, + ) + ) + + result = variants_service.get("TRV-001") + + assert result.to_dict() == variant_data + + +def test_extension_term_variants_list( + variants_service: ExtensionTermVariantsService, variant_data: dict +) -> None: + response_data = { + "data": [variant_data], + "$meta": {"pagination": {"total": 1, "offset": 0, "limit": 100}}, + } + with respx.mock: + respx.get(VARIANTS_URL).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + json=response_data, + ) + ) + + result = variants_service.fetch_page() + + assert result[0].to_dict() == variant_data + + +def test_extension_term_variants_service_endpoint( + variants_service: ExtensionTermVariantsService, +) -> None: + result = variants_service.path + + assert result == "/public/v1/integration/extensions/EXT-001/terms/TERM-001/variants" + + +def test_extension_terms_variants_accessor(terms_service: ExtensionTermsService) -> None: + result = terms_service.variants("TERM-001") + + assert isinstance(result, ExtensionTermVariantsService) + assert result.http_client == terms_service.http_client + assert result.endpoint_params == {"extension_id": "EXT-001", "term_id": "TERM-001"} From dc6ff6e265d330941ee28ab365c4562a21db859e Mon Sep 17 00:00:00 2001 From: Albert Sola Date: Tue, 7 Apr 2026 11:28:32 +0100 Subject: [PATCH 2/3] MPT-19907: add e2e tests for /public/v1/integration/extensions/{extensionId}/terms/{termId}/variants Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- e2e_config.test.json | 3 +- .../extension_term_variants/__init__.py | 0 .../extension_term_variants/conftest.py | 58 ++++++++++++++++++ .../test_async_extension_term_variants.py | 60 +++++++++++++++++++ .../test_sync_extension_term_variants.py | 50 ++++++++++++++++ 5 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/integration/extension_term_variants/__init__.py create mode 100644 tests/e2e/integration/extension_term_variants/conftest.py create mode 100644 tests/e2e/integration/extension_term_variants/test_async_extension_term_variants.py create mode 100644 tests/e2e/integration/extension_term_variants/test_sync_extension_term_variants.py diff --git a/e2e_config.test.json b/e2e_config.test.json index 5d0bde00..077f4847 100644 --- a/e2e_config.test.json +++ b/e2e_config.test.json @@ -67,5 +67,6 @@ "notifications.category.id": "NTC-6157-0397", "notifications.message.id": "MSG-0000-6215-1019-0139", "notifications.subscriber.id": "NTS-0829-7123-7123", - "integration.extension.id": "EXT-6587-4477" + "integration.extension.id": "EXT-6587-4477", + "integration.term.id": "TRM-0000-0000" } diff --git a/tests/e2e/integration/extension_term_variants/__init__.py b/tests/e2e/integration/extension_term_variants/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/e2e/integration/extension_term_variants/conftest.py b/tests/e2e/integration/extension_term_variants/conftest.py new file mode 100644 index 00000000..1d4a9973 --- /dev/null +++ b/tests/e2e/integration/extension_term_variants/conftest.py @@ -0,0 +1,58 @@ +import pytest + +from mpt_api_client.exceptions import MPTAPIError + + +@pytest.fixture(scope="session") +def extension_id(e2e_config): + return e2e_config["integration.extension.id"] + + +@pytest.fixture(scope="session") +def term_id(e2e_config): + return e2e_config["integration.term.id"] + + +@pytest.fixture +def extension_term_variants_service(mpt_ops, extension_id, term_id): + return mpt_ops.integration.extensions.terms(extension_id).variants(term_id) + + +@pytest.fixture +def async_extension_term_variants_service(async_mpt_ops, extension_id, term_id): + return async_mpt_ops.integration.extensions.terms(extension_id).variants(term_id) + + +@pytest.fixture +def variant_data(short_uuid): + return { + "type": "Online", + "assetUrl": "https://example.com/terms", + "languageCode": "en", + "name": f"e2e - please delete {short_uuid}", + "description": "Created by automated E2E tests. Safe to delete.", + } + + +@pytest.fixture +def created_variant(extension_term_variants_service, variant_data): + variant = extension_term_variants_service.create(variant_data) + + yield variant + + try: + extension_term_variants_service.delete(variant.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete variant {variant.id}: {error.title}") # noqa: WPS421 + + +@pytest.fixture +async def async_created_variant(async_extension_term_variants_service, variant_data): + variant = await async_extension_term_variants_service.create(variant_data) + + yield variant + + try: + await async_extension_term_variants_service.delete(variant.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete variant {variant.id}: {error.title}") # noqa: WPS421 diff --git a/tests/e2e/integration/extension_term_variants/test_async_extension_term_variants.py b/tests/e2e/integration/extension_term_variants/test_async_extension_term_variants.py new file mode 100644 index 00000000..dd766033 --- /dev/null +++ b/tests/e2e/integration/extension_term_variants/test_async_extension_term_variants.py @@ -0,0 +1,60 @@ +import pytest + +from tests.e2e.helper import assert_async_service_filter_with_iterate + +pytestmark = [ + pytest.mark.flaky, +] + + +@pytest.mark.skip(reason="creates real resources; run manually only") +def test_create_extension_term_variant(async_created_variant, variant_data): + result = async_created_variant.name + + assert result == variant_data["name"] + + +async def test_filter_extension_term_variants(async_extension_term_variants_service, term_id): + await assert_async_service_filter_with_iterate( + async_extension_term_variants_service, term_id, None + ) # act + + +@pytest.mark.skip(reason="modifies real resources; run manually only") +async def test_update_extension_term_variant( + async_extension_term_variants_service, async_created_variant, short_uuid +): + update_data = {"name": f"e2e updated {short_uuid}"} + + result = await async_extension_term_variants_service.update( + async_created_variant.id, update_data + ) + + assert result.name == update_data["name"] + + +@pytest.mark.skip(reason="modifies real resources; run manually only") +async def test_publish_extension_term_variant( + async_extension_term_variants_service, async_created_variant +): + result = await async_extension_term_variants_service.publish(async_created_variant.id) + + assert result.status == "Published" + + +@pytest.mark.skip(reason="modifies real resources; run manually only") +async def test_unpublish_extension_term_variant( + async_extension_term_variants_service, async_created_variant +): + await async_extension_term_variants_service.publish(async_created_variant.id) + + result = await async_extension_term_variants_service.unpublish(async_created_variant.id) + + assert result.status == "Unpublished" + + +@pytest.mark.skip(reason="deletes real resources; run manually only") +async def test_delete_extension_term_variant( + async_extension_term_variants_service, async_created_variant +): + await async_extension_term_variants_service.delete(async_created_variant.id) # act diff --git a/tests/e2e/integration/extension_term_variants/test_sync_extension_term_variants.py b/tests/e2e/integration/extension_term_variants/test_sync_extension_term_variants.py new file mode 100644 index 00000000..4d405db2 --- /dev/null +++ b/tests/e2e/integration/extension_term_variants/test_sync_extension_term_variants.py @@ -0,0 +1,50 @@ +import pytest + +from tests.e2e.helper import assert_service_filter_with_iterate + +pytestmark = [ + pytest.mark.flaky, +] + + +@pytest.mark.skip(reason="creates real resources; run manually only") +def test_create_extension_term_variant(created_variant, variant_data): + result = created_variant.name + + assert result == variant_data["name"] + + +def test_filter_extension_term_variants(extension_term_variants_service, term_id): + assert_service_filter_with_iterate(extension_term_variants_service, term_id, None) # act + + +@pytest.mark.skip(reason="modifies real resources; run manually only") +def test_update_extension_term_variant( + extension_term_variants_service, created_variant, short_uuid +): + update_data = {"name": f"e2e updated {short_uuid}"} + + result = extension_term_variants_service.update(created_variant.id, update_data) + + assert result.name == update_data["name"] + + +@pytest.mark.skip(reason="modifies real resources; run manually only") +def test_publish_extension_term_variant(extension_term_variants_service, created_variant): + result = extension_term_variants_service.publish(created_variant.id) + + assert result.status == "Published" + + +@pytest.mark.skip(reason="modifies real resources; run manually only") +def test_unpublish_extension_term_variant(extension_term_variants_service, created_variant): + extension_term_variants_service.publish(created_variant.id) + + result = extension_term_variants_service.unpublish(created_variant.id) + + assert result.status == "Unpublished" + + +@pytest.mark.skip(reason="deletes real resources; run manually only") +def test_delete_extension_term_variant(extension_term_variants_service, created_variant): + extension_term_variants_service.delete(created_variant.id) # act From 89765512c8734a74515cadd135ea4b54b97d6362 Mon Sep 17 00:00:00 2001 From: Albert Sola Date: Fri, 10 Apr 2026 13:37:42 +0100 Subject: [PATCH 3/3] MPT-19907: update e2e to upload pdf file for variant creation --- e2e_config.test.json | 2 +- .../extension_term_variants/conftest.py | 22 +++++++++---------- .../test_async_extension_term_variants.py | 11 ++++------ .../test_sync_extension_term_variants.py | 11 ++++------ 4 files changed, 20 insertions(+), 26 deletions(-) diff --git a/e2e_config.test.json b/e2e_config.test.json index 077f4847..b999baef 100644 --- a/e2e_config.test.json +++ b/e2e_config.test.json @@ -68,5 +68,5 @@ "notifications.message.id": "MSG-0000-6215-1019-0139", "notifications.subscriber.id": "NTS-0829-7123-7123", "integration.extension.id": "EXT-6587-4477", - "integration.term.id": "TRM-0000-0000" + "integration.term.id": "ETC-6587-4477-0062" } diff --git a/tests/e2e/integration/extension_term_variants/conftest.py b/tests/e2e/integration/extension_term_variants/conftest.py index 1d4a9973..500337cb 100644 --- a/tests/e2e/integration/extension_term_variants/conftest.py +++ b/tests/e2e/integration/extension_term_variants/conftest.py @@ -14,29 +14,29 @@ def term_id(e2e_config): @pytest.fixture -def extension_term_variants_service(mpt_ops, extension_id, term_id): - return mpt_ops.integration.extensions.terms(extension_id).variants(term_id) +def extension_term_variants_service(mpt_vendor, extension_id, term_id): + return mpt_vendor.integration.extensions.terms(extension_id).variants(term_id) @pytest.fixture -def async_extension_term_variants_service(async_mpt_ops, extension_id, term_id): - return async_mpt_ops.integration.extensions.terms(extension_id).variants(term_id) +def async_extension_term_variants_service(async_mpt_vendor, extension_id, term_id): + return async_mpt_vendor.integration.extensions.terms(extension_id).variants(term_id) @pytest.fixture def variant_data(short_uuid): return { - "type": "Online", - "assetUrl": "https://example.com/terms", - "languageCode": "en", + "type": "File", + "assetUrl": "", + "languageCode": "en-US", "name": f"e2e - please delete {short_uuid}", "description": "Created by automated E2E tests. Safe to delete.", } @pytest.fixture -def created_variant(extension_term_variants_service, variant_data): - variant = extension_term_variants_service.create(variant_data) +def created_variant(extension_term_variants_service, variant_data, pdf_fd): + variant = extension_term_variants_service.create(variant_data, file=pdf_fd) yield variant @@ -47,8 +47,8 @@ def created_variant(extension_term_variants_service, variant_data): @pytest.fixture -async def async_created_variant(async_extension_term_variants_service, variant_data): - variant = await async_extension_term_variants_service.create(variant_data) +async def async_created_variant(async_extension_term_variants_service, variant_data, pdf_fd): + variant = await async_extension_term_variants_service.create(variant_data, file=pdf_fd) yield variant diff --git a/tests/e2e/integration/extension_term_variants/test_async_extension_term_variants.py b/tests/e2e/integration/extension_term_variants/test_async_extension_term_variants.py index dd766033..296faa8e 100644 --- a/tests/e2e/integration/extension_term_variants/test_async_extension_term_variants.py +++ b/tests/e2e/integration/extension_term_variants/test_async_extension_term_variants.py @@ -7,20 +7,20 @@ ] -@pytest.mark.skip(reason="creates real resources; run manually only") def test_create_extension_term_variant(async_created_variant, variant_data): result = async_created_variant.name assert result == variant_data["name"] -async def test_filter_extension_term_variants(async_extension_term_variants_service, term_id): +async def test_filter_extension_term_variants( + async_extension_term_variants_service, async_created_variant +): await assert_async_service_filter_with_iterate( - async_extension_term_variants_service, term_id, None + async_extension_term_variants_service, async_created_variant.id, None ) # act -@pytest.mark.skip(reason="modifies real resources; run manually only") async def test_update_extension_term_variant( async_extension_term_variants_service, async_created_variant, short_uuid ): @@ -33,7 +33,6 @@ async def test_update_extension_term_variant( assert result.name == update_data["name"] -@pytest.mark.skip(reason="modifies real resources; run manually only") async def test_publish_extension_term_variant( async_extension_term_variants_service, async_created_variant ): @@ -42,7 +41,6 @@ async def test_publish_extension_term_variant( assert result.status == "Published" -@pytest.mark.skip(reason="modifies real resources; run manually only") async def test_unpublish_extension_term_variant( async_extension_term_variants_service, async_created_variant ): @@ -53,7 +51,6 @@ async def test_unpublish_extension_term_variant( assert result.status == "Unpublished" -@pytest.mark.skip(reason="deletes real resources; run manually only") async def test_delete_extension_term_variant( async_extension_term_variants_service, async_created_variant ): diff --git a/tests/e2e/integration/extension_term_variants/test_sync_extension_term_variants.py b/tests/e2e/integration/extension_term_variants/test_sync_extension_term_variants.py index 4d405db2..4849fcbb 100644 --- a/tests/e2e/integration/extension_term_variants/test_sync_extension_term_variants.py +++ b/tests/e2e/integration/extension_term_variants/test_sync_extension_term_variants.py @@ -7,18 +7,18 @@ ] -@pytest.mark.skip(reason="creates real resources; run manually only") def test_create_extension_term_variant(created_variant, variant_data): result = created_variant.name assert result == variant_data["name"] -def test_filter_extension_term_variants(extension_term_variants_service, term_id): - assert_service_filter_with_iterate(extension_term_variants_service, term_id, None) # act +def test_filter_extension_term_variants(extension_term_variants_service, created_variant): + assert_service_filter_with_iterate( + extension_term_variants_service, created_variant.id, None + ) # act -@pytest.mark.skip(reason="modifies real resources; run manually only") def test_update_extension_term_variant( extension_term_variants_service, created_variant, short_uuid ): @@ -29,14 +29,12 @@ def test_update_extension_term_variant( assert result.name == update_data["name"] -@pytest.mark.skip(reason="modifies real resources; run manually only") def test_publish_extension_term_variant(extension_term_variants_service, created_variant): result = extension_term_variants_service.publish(created_variant.id) assert result.status == "Published" -@pytest.mark.skip(reason="modifies real resources; run manually only") def test_unpublish_extension_term_variant(extension_term_variants_service, created_variant): extension_term_variants_service.publish(created_variant.id) @@ -45,6 +43,5 @@ def test_unpublish_extension_term_variant(extension_term_variants_service, creat assert result.status == "Unpublished" -@pytest.mark.skip(reason="deletes real resources; run manually only") def test_delete_extension_term_variant(extension_term_variants_service, created_variant): extension_term_variants_service.delete(created_variant.id) # act