From 59781302cf5a5cef462bf592cbf55ac6fbc237f5 Mon Sep 17 00:00:00 2001 From: Albert Sola Date: Mon, 17 Nov 2025 12:02:39 +0000 Subject: [PATCH] MPT-14889: Implement E2E Tests and Enhance Document Handling - Added end-to-end tests for catalog/product document workflows - Refactored document service to improve usability and interface consistency - Introduced support for both URL-based and file-based document uploads - Updated download logic to include Accept header and retrieve it from the resource when required - Enabled follow_redirects on the http client --- e2e_config.test.json | 1 + mpt_api_client/http/async_client.py | 1 + mpt_api_client/http/async_service.py | 8 +- mpt_api_client/http/client.py | 1 + mpt_api_client/http/mixins.py | 83 ++++++---- mpt_api_client/http/service.py | 3 +- .../billing/credit_memo_attachments.py | 8 +- .../billing/custom_ledger_attachments.py | 8 +- .../resources/billing/custom_ledger_upload.py | 6 +- .../resources/billing/invoice_attachments.py | 8 +- .../resources/billing/journal_attachments.py | 8 +- .../resources/billing/journal_upload.py | 8 +- .../resources/billing/ledger_attachments.py | 8 +- mpt_api_client/resources/catalog/mixins.py | 72 +++++++++ .../catalog/pricing_policy_attachments.py | 8 +- .../catalog/product_term_variants.py | 8 +- .../resources/catalog/products_documents.py | 13 +- .../resources/catalog/products_media.py | 8 +- .../commerce/agreements_attachments.py | 8 +- setup.cfg | 1 + .../e2e/catalog/product/documents/__init__.py | 0 .../e2e/catalog/product/documents/conftest.py | 33 ++++ .../product/documents/test_async_document.py | 102 ++++++++++++ .../product/documents/test_sync_document.py | 94 +++++++++++ tests/e2e/conftest.py | 11 ++ tests/e2e/empty.pdf | Bin 0 -> 3840 bytes tests/unit/http/test_async_client.py | 2 + tests/unit/http/test_client.py | 2 + tests/unit/http/test_mixins.py | 63 ++++++-- tests/unit/resources/catalog/test_mixins.py | 150 ++++++++++++++++++ 30 files changed, 628 insertions(+), 98 deletions(-) create mode 100644 tests/e2e/catalog/product/documents/__init__.py create mode 100644 tests/e2e/catalog/product/documents/conftest.py create mode 100644 tests/e2e/catalog/product/documents/test_async_document.py create mode 100644 tests/e2e/catalog/product/documents/test_sync_document.py create mode 100644 tests/e2e/empty.pdf diff --git a/e2e_config.test.json b/e2e_config.test.json index a93317cb..6850dc32 100644 --- a/e2e_config.test.json +++ b/e2e_config.test.json @@ -3,6 +3,7 @@ "accounts.seller.id": "SEL-7310-3075", "catalog.product.parameter_group.id": "PGR-7255-3950-0001", "catalog.product.parameter.id": "PAR-7255-3950-0016", + "catalog.product.document.id": "PDC-7255-3950-0001", "accounts.account.id": "ACC-9042-0088", "accounts.buyer.account.id": "ACC-1086-6867", "accounts.buyer.id": "BUY-1591-2112", diff --git a/mpt_api_client/http/async_client.py b/mpt_api_client/http/async_client.py index 1de2f23d..d0f74b71 100644 --- a/mpt_api_client/http/async_client.py +++ b/mpt_api_client/http/async_client.py @@ -53,6 +53,7 @@ def __init__( headers=base_headers, timeout=timeout, transport=AsyncHTTPTransport(retries=retries), + follow_redirects=True, ) async def request( # noqa: WPS211 diff --git a/mpt_api_client/http/async_service.py b/mpt_api_client/http/async_service.py index 5749b81e..585b674d 100644 --- a/mpt_api_client/http/async_service.py +++ b/mpt_api_client/http/async_service.py @@ -1,5 +1,6 @@ from urllib.parse import urljoin +from mpt_api_client.constants import APPLICATION_JSON from mpt_api_client.http.async_client import AsyncHTTPClient from mpt_api_client.http.base_service import ServiceBase from mpt_api_client.http.types import QueryParam, Response @@ -69,6 +70,11 @@ async def _resource_action( query_params: Additional query parameters. """ response = await self._resource_do_request( - resource_id, method, action, json=json, query_params=query_params + resource_id, + method, + action, + json=json, + query_params=query_params, + headers={"Accept": APPLICATION_JSON}, ) return self._model_class.from_response(response) diff --git a/mpt_api_client/http/client.py b/mpt_api_client/http/client.py index ee8aa05e..f623a613 100644 --- a/mpt_api_client/http/client.py +++ b/mpt_api_client/http/client.py @@ -55,6 +55,7 @@ def __init__( headers=base_headers, timeout=timeout, transport=HTTPTransport(retries=retries), + follow_redirects=True, ) def request( # noqa: WPS211 diff --git a/mpt_api_client/http/mixins.py b/mpt_api_client/http/mixins.py index 2ea73b84..66d22dad 100644 --- a/mpt_api_client/http/mixins.py +++ b/mpt_api_client/http/mixins.py @@ -4,6 +4,7 @@ from urllib.parse import urljoin from mpt_api_client.constants import APPLICATION_JSON +from mpt_api_client.exceptions import MPTError from mpt_api_client.http.query_state import QueryState from mpt_api_client.http.types import FileTypes, Response from mpt_api_client.models import Collection, FileModel, ResourceData @@ -60,7 +61,32 @@ def update(self, resource_id: str, resource_data: ResourceData) -> Model: return self._resource_action(resource_id, "PUT", json=resource_data) # type: ignore[attr-defined, no-any-return] -class FileOperationsMixin[Model]: +class DownloadFileMixin[Model]: + """Download file mixin.""" + + def download(self, resource_id: str, accept: str | None = None) -> FileModel: + """Download the file for the given resource ID. + + Args: + resource_id: Resource ID. + accept: The content type expected for the file. + If not provided, the content type will be fetched from the resource. + + Returns: + File model containing the downloaded file. + """ + if not accept: + resource: Model = self._resource_action(resource_id, method="GET") # type: ignore[attr-defined] + accept = resource.content_type # type: ignore[attr-defined] + if not accept: + raise MPTError("Unable to download file. Content type not found in resource") + response: Response = self._resource_do_request( # type: ignore[attr-defined] + resource_id, method="GET", headers={"Accept": accept} + ) + return FileModel(response) + + +class FilesOperationsMixin[Model](DownloadFileMixin[Model]): """Mixin that provides create and download methods for file-based resources.""" def create( @@ -91,20 +117,6 @@ def create( return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return] - def download(self, resource_id: str) -> FileModel: - """Download the file for the given resource ID. - - Args: - resource_id: Resource ID. - - Returns: - File model containing the downloaded file. - """ - response: Response = self._resource_do_request( # type: ignore[attr-defined] - resource_id, method="GET", headers={"Accept": "*"} - ) - return FileModel(response) - class CreateWithIconMixin[Model]: """Create resource with icon mixin.""" @@ -221,7 +233,32 @@ async def update(self, resource_id: str, resource_data: ResourceData) -> Model: return await self._resource_action(resource_id, "PUT", json=resource_data) # type: ignore[attr-defined, no-any-return] -class AsyncFileOperationsMixin[Model]: +class AsyncDownloadFileMixin[Model]: + """Download file mixin.""" + + async def download(self, resource_id: str, accept: str | None = None) -> FileModel: + """Download the file for the given resource ID. + + Args: + resource_id: Resource ID. + accept: The content type expected for the file. + If not provided, the content type will be fetched from the resource. + + Returns: + File model containing the downloaded file. + """ + if not accept: + resource: Model = await self._resource_action(resource_id, method="GET") # type: ignore[attr-defined] + accept = resource.content_type # type: ignore[attr-defined] + if not accept: + raise MPTError("Unable to download file. Content type not found in resource") + response = await self._resource_do_request( # type: ignore[attr-defined] + resource_id, method="GET", headers={"Accept": accept} + ) + return FileModel(response) + + +class AsyncFilesOperationsMixin[Model](AsyncDownloadFileMixin[Model]): """Async mixin that provides create and download methods for file-based resources.""" async def create( @@ -253,20 +290,6 @@ async def create( return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return] - async def download(self, resource_id: str) -> FileModel: - """Download the file for the given resource ID. - - Args: - resource_id: Resource ID. - - Returns: - File model containing the downloaded file. - """ - response = await self._resource_do_request( # type: ignore[attr-defined] - resource_id, method="GET", headers={"Accept": "*"} - ) - return FileModel(response) - class AsyncCreateWithIconMixin[Model]: """Create resource with icon mixin.""" diff --git a/mpt_api_client/http/service.py b/mpt_api_client/http/service.py index 00162823..45ae6ac8 100644 --- a/mpt_api_client/http/service.py +++ b/mpt_api_client/http/service.py @@ -1,5 +1,6 @@ from urllib.parse import urljoin +from mpt_api_client.constants import APPLICATION_JSON from mpt_api_client.http.base_service import ServiceBase from mpt_api_client.http.client import HTTPClient from mpt_api_client.http.types import QueryParam, Response @@ -74,6 +75,6 @@ def _resource_action( action, json=json, query_params=query_params, - headers={"Accept": "application/json"}, + headers={"Accept": APPLICATION_JSON}, ) return self._model_class.from_response(response) diff --git a/mpt_api_client/resources/billing/credit_memo_attachments.py b/mpt_api_client/resources/billing/credit_memo_attachments.py index 65e020fa..3805efe2 100644 --- a/mpt_api_client/resources/billing/credit_memo_attachments.py +++ b/mpt_api_client/resources/billing/credit_memo_attachments.py @@ -1,10 +1,10 @@ from mpt_api_client.http import AsyncService, Service from mpt_api_client.http.mixins import ( AsyncCollectionMixin, - AsyncFileOperationsMixin, + AsyncFilesOperationsMixin, AsyncModifiableResourceMixin, CollectionMixin, - FileOperationsMixin, + FilesOperationsMixin, ModifiableResourceMixin, ) from mpt_api_client.models import Model @@ -23,7 +23,7 @@ class CreditMemoAttachmentsServiceConfig: class CreditMemoAttachmentsService( - FileOperationsMixin[CreditMemoAttachment], + FilesOperationsMixin[CreditMemoAttachment], ModifiableResourceMixin[CreditMemoAttachment], CollectionMixin[CreditMemoAttachment], Service[CreditMemoAttachment], @@ -33,7 +33,7 @@ class CreditMemoAttachmentsService( class AsyncCreditMemoAttachmentsService( - AsyncFileOperationsMixin[CreditMemoAttachment], + AsyncFilesOperationsMixin[CreditMemoAttachment], AsyncModifiableResourceMixin[CreditMemoAttachment], AsyncCollectionMixin[CreditMemoAttachment], AsyncService[CreditMemoAttachment], diff --git a/mpt_api_client/resources/billing/custom_ledger_attachments.py b/mpt_api_client/resources/billing/custom_ledger_attachments.py index 5fda68da..45fb3918 100644 --- a/mpt_api_client/resources/billing/custom_ledger_attachments.py +++ b/mpt_api_client/resources/billing/custom_ledger_attachments.py @@ -1,10 +1,10 @@ from mpt_api_client.http import AsyncService, Service from mpt_api_client.http.mixins import ( AsyncCollectionMixin, - AsyncFileOperationsMixin, + AsyncFilesOperationsMixin, AsyncModifiableResourceMixin, CollectionMixin, - FileOperationsMixin, + FilesOperationsMixin, ModifiableResourceMixin, ) from mpt_api_client.models import Model @@ -23,7 +23,7 @@ class CustomLedgerAttachmentsServiceConfig: class CustomLedgerAttachmentsService( - FileOperationsMixin[CustomLedgerAttachment], + FilesOperationsMixin[CustomLedgerAttachment], ModifiableResourceMixin[CustomLedgerAttachment], CollectionMixin[CustomLedgerAttachment], Service[CustomLedgerAttachment], @@ -33,7 +33,7 @@ class CustomLedgerAttachmentsService( class AsyncCustomLedgerAttachmentsService( - AsyncFileOperationsMixin[CustomLedgerAttachment], + AsyncFilesOperationsMixin[CustomLedgerAttachment], AsyncModifiableResourceMixin[CustomLedgerAttachment], AsyncCollectionMixin[CustomLedgerAttachment], AsyncService[CustomLedgerAttachment], diff --git a/mpt_api_client/resources/billing/custom_ledger_upload.py b/mpt_api_client/resources/billing/custom_ledger_upload.py index 2c445b3a..47351df2 100644 --- a/mpt_api_client/resources/billing/custom_ledger_upload.py +++ b/mpt_api_client/resources/billing/custom_ledger_upload.py @@ -1,5 +1,5 @@ from mpt_api_client.http import AsyncService, Service -from mpt_api_client.http.mixins import AsyncFileOperationsMixin, FileOperationsMixin +from mpt_api_client.http.mixins import AsyncFilesOperationsMixin, FilesOperationsMixin from mpt_api_client.models import Model @@ -16,7 +16,7 @@ class CustomLedgerUploadServiceConfig: class CustomLedgerUploadService( - FileOperationsMixin[CustomLedgerUpload], + FilesOperationsMixin[CustomLedgerUpload], Service[CustomLedgerUpload], CustomLedgerUploadServiceConfig, ): @@ -24,7 +24,7 @@ class CustomLedgerUploadService( class AsyncCustomLedgerUploadService( - AsyncFileOperationsMixin[CustomLedgerUpload], + AsyncFilesOperationsMixin[CustomLedgerUpload], AsyncService[CustomLedgerUpload], CustomLedgerUploadServiceConfig, ): diff --git a/mpt_api_client/resources/billing/invoice_attachments.py b/mpt_api_client/resources/billing/invoice_attachments.py index ba9cd800..7966f731 100644 --- a/mpt_api_client/resources/billing/invoice_attachments.py +++ b/mpt_api_client/resources/billing/invoice_attachments.py @@ -1,10 +1,10 @@ from mpt_api_client.http import AsyncService, Service from mpt_api_client.http.mixins import ( AsyncCollectionMixin, - AsyncFileOperationsMixin, + AsyncFilesOperationsMixin, AsyncModifiableResourceMixin, CollectionMixin, - FileOperationsMixin, + FilesOperationsMixin, ModifiableResourceMixin, ) from mpt_api_client.models import Model @@ -23,7 +23,7 @@ class InvoiceAttachmentsServiceConfig: class InvoiceAttachmentsService( - FileOperationsMixin[InvoiceAttachment], + FilesOperationsMixin[InvoiceAttachment], ModifiableResourceMixin[InvoiceAttachment], CollectionMixin[InvoiceAttachment], Service[InvoiceAttachment], @@ -33,7 +33,7 @@ class InvoiceAttachmentsService( class AsyncInvoiceAttachmentsService( - AsyncFileOperationsMixin[InvoiceAttachment], + AsyncFilesOperationsMixin[InvoiceAttachment], AsyncModifiableResourceMixin[InvoiceAttachment], AsyncCollectionMixin[InvoiceAttachment], AsyncService[InvoiceAttachment], diff --git a/mpt_api_client/resources/billing/journal_attachments.py b/mpt_api_client/resources/billing/journal_attachments.py index 6fc50ba3..fcf81a1d 100644 --- a/mpt_api_client/resources/billing/journal_attachments.py +++ b/mpt_api_client/resources/billing/journal_attachments.py @@ -1,10 +1,10 @@ from mpt_api_client.http import AsyncService, Service from mpt_api_client.http.mixins import ( AsyncCollectionMixin, - AsyncFileOperationsMixin, + AsyncFilesOperationsMixin, AsyncModifiableResourceMixin, CollectionMixin, - FileOperationsMixin, + FilesOperationsMixin, ModifiableResourceMixin, ) from mpt_api_client.models import Model @@ -23,7 +23,7 @@ class JournalAttachmentsServiceConfig: class JournalAttachmentsService( - FileOperationsMixin[JournalAttachment], + FilesOperationsMixin[JournalAttachment], ModifiableResourceMixin[JournalAttachment], CollectionMixin[JournalAttachment], Service[JournalAttachment], @@ -33,7 +33,7 @@ class JournalAttachmentsService( class AsyncJournalAttachmentsService( - AsyncFileOperationsMixin[JournalAttachment], + AsyncFilesOperationsMixin[JournalAttachment], AsyncModifiableResourceMixin[JournalAttachment], AsyncCollectionMixin[JournalAttachment], AsyncService[JournalAttachment], diff --git a/mpt_api_client/resources/billing/journal_upload.py b/mpt_api_client/resources/billing/journal_upload.py index da661985..dd9e9774 100644 --- a/mpt_api_client/resources/billing/journal_upload.py +++ b/mpt_api_client/resources/billing/journal_upload.py @@ -1,9 +1,9 @@ from mpt_api_client.http import AsyncService, Service from mpt_api_client.http.mixins import ( AsyncCollectionMixin, - AsyncFileOperationsMixin, + AsyncFilesOperationsMixin, CollectionMixin, - FileOperationsMixin, + FilesOperationsMixin, ) from mpt_api_client.models import Model @@ -21,7 +21,7 @@ class JournalUploadServiceConfig: class JournalUploadService( - FileOperationsMixin[JournalUpload], + FilesOperationsMixin[JournalUpload], CollectionMixin[JournalUpload], Service[JournalUpload], JournalUploadServiceConfig, @@ -30,7 +30,7 @@ class JournalUploadService( class AsyncJournalUploadService( - AsyncFileOperationsMixin[JournalUpload], + AsyncFilesOperationsMixin[JournalUpload], AsyncCollectionMixin[JournalUpload], AsyncService[JournalUpload], JournalUploadServiceConfig, diff --git a/mpt_api_client/resources/billing/ledger_attachments.py b/mpt_api_client/resources/billing/ledger_attachments.py index 6f195868..836ac718 100644 --- a/mpt_api_client/resources/billing/ledger_attachments.py +++ b/mpt_api_client/resources/billing/ledger_attachments.py @@ -1,10 +1,10 @@ from mpt_api_client.http import AsyncService, Service from mpt_api_client.http.mixins import ( AsyncCollectionMixin, - AsyncFileOperationsMixin, + AsyncFilesOperationsMixin, AsyncModifiableResourceMixin, CollectionMixin, - FileOperationsMixin, + FilesOperationsMixin, ModifiableResourceMixin, ) from mpt_api_client.models import Model @@ -23,7 +23,7 @@ class LedgerAttachmentsServiceConfig: class LedgerAttachmentsService( - FileOperationsMixin[LedgerAttachment], + FilesOperationsMixin[LedgerAttachment], ModifiableResourceMixin[LedgerAttachment], CollectionMixin[LedgerAttachment], Service[LedgerAttachment], @@ -33,7 +33,7 @@ class LedgerAttachmentsService( class AsyncLedgerAttachmentsService( - AsyncFileOperationsMixin[LedgerAttachment], + AsyncFilesOperationsMixin[LedgerAttachment], AsyncModifiableResourceMixin[LedgerAttachment], AsyncCollectionMixin[LedgerAttachment], AsyncService[LedgerAttachment], diff --git a/mpt_api_client/resources/catalog/mixins.py b/mpt_api_client/resources/catalog/mixins.py index 639f1b99..5e5bb789 100644 --- a/mpt_api_client/resources/catalog/mixins.py +++ b/mpt_api_client/resources/catalog/mixins.py @@ -1,3 +1,10 @@ +from mpt_api_client.constants import APPLICATION_JSON +from mpt_api_client.http.mixins import ( + AsyncDownloadFileMixin, + DownloadFileMixin, + _json_to_file_payload, +) +from mpt_api_client.http.types import FileTypes from mpt_api_client.models import ResourceData @@ -76,6 +83,71 @@ async def unpublish(self, resource_id: str, resource_data: ResourceData | None = ) +class AsyncDocumentMixin[Model]( + AsyncDownloadFileMixin[Model], + AsyncPublishableMixin[Model], +): + """Async document mixin.""" + + async def create(self, resource_data: ResourceData, file: FileTypes | None = None) -> Model: + """Creates document resource. + + Creates a document resource by specifying a `file` or an `url`. + + Args: + resource_data: Resource data. + file: File to upload. + + Returns: + Created resource. + + """ + files = {} + + if resource_data: + files["document"] = ( + None, + _json_to_file_payload(resource_data), + APPLICATION_JSON, + ) + if file: + files["file"] = file # type: ignore[assignment] + response = await self.http_client.request("post", self.path, files=files) # type: ignore[attr-defined] + return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return] + + +class DocumentMixin[Model]( + DownloadFileMixin[Model], + PublishableMixin[Model], +): + """Document mixin.""" + + def create(self, resource_data: ResourceData, file: FileTypes | None = None) -> Model: + """Create document. + + Creates a document resource by specifying a `file` or an `url`. + + Args: + resource_data: Resource data. + file: File to upload. + + Returns: + Created resource. + """ + files = {} + + if resource_data: + files["document"] = ( + None, + _json_to_file_payload(resource_data), + APPLICATION_JSON, + ) + if file: + files["file"] = file # type: ignore[assignment] + response = self.http_client.request("post", self.path, files=files) # type: ignore[attr-defined] + return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return] + + class ActivatableMixin[Model]: """Activatable mixin adds the ability to activate and deactivate.""" diff --git a/mpt_api_client/resources/catalog/pricing_policy_attachments.py b/mpt_api_client/resources/catalog/pricing_policy_attachments.py index 72042bea..f237697a 100644 --- a/mpt_api_client/resources/catalog/pricing_policy_attachments.py +++ b/mpt_api_client/resources/catalog/pricing_policy_attachments.py @@ -1,10 +1,10 @@ from mpt_api_client.http import AsyncService, Service from mpt_api_client.http.mixins import ( AsyncCollectionMixin, - AsyncFileOperationsMixin, + AsyncFilesOperationsMixin, AsyncModifiableResourceMixin, CollectionMixin, - FileOperationsMixin, + FilesOperationsMixin, ModifiableResourceMixin, ) from mpt_api_client.models import Model @@ -24,7 +24,7 @@ class PricingPolicyAttachmentsServiceConfig: class PricingPolicyAttachmentsService( - FileOperationsMixin[PricingPolicyAttachment], + FilesOperationsMixin[PricingPolicyAttachment], ActivatableMixin[PricingPolicyAttachment], ModifiableResourceMixin[PricingPolicyAttachment], CollectionMixin[PricingPolicyAttachment], @@ -35,7 +35,7 @@ class PricingPolicyAttachmentsService( class AsyncPricingPolicyAttachmentsService( - AsyncFileOperationsMixin[PricingPolicyAttachment], + AsyncFilesOperationsMixin[PricingPolicyAttachment], AsyncActivatableMixin[PricingPolicyAttachment], AsyncModifiableResourceMixin[PricingPolicyAttachment], AsyncCollectionMixin[PricingPolicyAttachment], diff --git a/mpt_api_client/resources/catalog/product_term_variants.py b/mpt_api_client/resources/catalog/product_term_variants.py index 12ef4e5f..2edae1ab 100644 --- a/mpt_api_client/resources/catalog/product_term_variants.py +++ b/mpt_api_client/resources/catalog/product_term_variants.py @@ -1,10 +1,10 @@ from mpt_api_client.http import AsyncService, Service from mpt_api_client.http.mixins import ( AsyncCollectionMixin, - AsyncFileOperationsMixin, + AsyncFilesOperationsMixin, AsyncModifiableResourceMixin, CollectionMixin, - FileOperationsMixin, + FilesOperationsMixin, ModifiableResourceMixin, ) from mpt_api_client.models import Model @@ -27,7 +27,7 @@ class TermVariantServiceConfig: class TermVariantService( - FileOperationsMixin[TermVariant], + FilesOperationsMixin[TermVariant], ModifiableResourceMixin[TermVariant], PublishableMixin[TermVariant], CollectionMixin[TermVariant], @@ -38,7 +38,7 @@ class TermVariantService( class AsyncTermVariantService( - AsyncFileOperationsMixin[TermVariant], + AsyncFilesOperationsMixin[TermVariant], AsyncModifiableResourceMixin[TermVariant], AsyncPublishableMixin[TermVariant], AsyncCollectionMixin[TermVariant], diff --git a/mpt_api_client/resources/catalog/products_documents.py b/mpt_api_client/resources/catalog/products_documents.py index f903fc8f..01ce52c2 100644 --- a/mpt_api_client/resources/catalog/products_documents.py +++ b/mpt_api_client/resources/catalog/products_documents.py @@ -1,14 +1,15 @@ from mpt_api_client.http import AsyncService, Service from mpt_api_client.http.mixins import ( AsyncCollectionMixin, - AsyncFileOperationsMixin, AsyncModifiableResourceMixin, CollectionMixin, - FileOperationsMixin, ModifiableResourceMixin, ) from mpt_api_client.models import Model -from mpt_api_client.resources.catalog.mixins import AsyncPublishableMixin, PublishableMixin +from mpt_api_client.resources.catalog.mixins import ( + AsyncDocumentMixin, + DocumentMixin, +) class Document(Model): @@ -24,8 +25,7 @@ class DocumentServiceConfig: class DocumentService( - FileOperationsMixin[Document], - PublishableMixin[Document], + DocumentMixin[Document], ModifiableResourceMixin[Document], CollectionMixin[Document], Service[Document], @@ -35,8 +35,7 @@ class DocumentService( class AsyncDocumentService( - AsyncFileOperationsMixin[Document], - AsyncPublishableMixin[Document], + AsyncDocumentMixin[Document], AsyncModifiableResourceMixin[Document], AsyncCollectionMixin[Document], AsyncService[Document], diff --git a/mpt_api_client/resources/catalog/products_media.py b/mpt_api_client/resources/catalog/products_media.py index 21ddf5f0..36cb7b27 100644 --- a/mpt_api_client/resources/catalog/products_media.py +++ b/mpt_api_client/resources/catalog/products_media.py @@ -5,10 +5,10 @@ from mpt_api_client.http import AsyncService, Service from mpt_api_client.http.mixins import ( AsyncCollectionMixin, - AsyncFileOperationsMixin, + AsyncFilesOperationsMixin, AsyncModifiableResourceMixin, CollectionMixin, - FileOperationsMixin, + FilesOperationsMixin, ModifiableResourceMixin, ) from mpt_api_client.models import Model, ResourceData @@ -31,7 +31,7 @@ class MediaServiceConfig: class MediaService( - FileOperationsMixin[Media], + FilesOperationsMixin[Media], PublishableMixin[Media], ModifiableResourceMixin[Media], CollectionMixin[Media], @@ -84,7 +84,7 @@ def create( class AsyncMediaService( - AsyncFileOperationsMixin[Media], + AsyncFilesOperationsMixin[Media], AsyncPublishableMixin[Media], AsyncModifiableResourceMixin[Media], AsyncCollectionMixin[Media], diff --git a/mpt_api_client/resources/commerce/agreements_attachments.py b/mpt_api_client/resources/commerce/agreements_attachments.py index 5042b88d..55d3e675 100644 --- a/mpt_api_client/resources/commerce/agreements_attachments.py +++ b/mpt_api_client/resources/commerce/agreements_attachments.py @@ -2,11 +2,11 @@ from mpt_api_client.http.mixins import ( AsyncCollectionMixin, AsyncDeleteMixin, - AsyncFileOperationsMixin, + AsyncFilesOperationsMixin, AsyncGetMixin, CollectionMixin, DeleteMixin, - FileOperationsMixin, + FilesOperationsMixin, GetMixin, ) from mpt_api_client.models import Model @@ -25,7 +25,7 @@ class AgreementsAttachmentServiceConfig: class AgreementsAttachmentService( - FileOperationsMixin[AgreementAttachment], + FilesOperationsMixin[AgreementAttachment], DeleteMixin, GetMixin[AgreementAttachment], CollectionMixin[AgreementAttachment], @@ -36,7 +36,7 @@ class AgreementsAttachmentService( class AsyncAgreementsAttachmentService( - AsyncFileOperationsMixin[AgreementAttachment], + AsyncFilesOperationsMixin[AgreementAttachment], AsyncDeleteMixin, AsyncGetMixin[AgreementAttachment], AsyncCollectionMixin[AgreementAttachment], diff --git a/setup.cfg b/setup.cfg index 06e1231e..96f96f7b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -50,6 +50,7 @@ per-file-ignores = tests/unit/test_mpt_client.py: WPS235 tests/e2e/accounts/*.py: WPS430 WPS202 tests/e2e/catalog/*.py: WPS421 + tests/e2e/catalog/product/documents/*.py: WPS202 WPS421 tests/*: # Allow magic strings. diff --git a/tests/e2e/catalog/product/documents/__init__.py b/tests/e2e/catalog/product/documents/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/e2e/catalog/product/documents/conftest.py b/tests/e2e/catalog/product/documents/conftest.py new file mode 100644 index 00000000..dd6f1bc8 --- /dev/null +++ b/tests/e2e/catalog/product/documents/conftest.py @@ -0,0 +1,33 @@ +import pathlib + +import pytest + + +@pytest.fixture +def document_id(e2e_config): + return e2e_config["catalog.product.document.id"] + + +@pytest.fixture +def document_data(): + return { + "name": "e2e test document - please delete", + "description": "E2E test document for automated testing", + "language": "en-gb", + } + + +@pytest.fixture +def test_file(): + file_path = pathlib.Path(__file__).parents[3] / "logo.png" + return pathlib.Path.open(file_path, "rb") + + +@pytest.fixture +def vendor_document_service(mpt_vendor, product_id): + return mpt_vendor.catalog.products.documents(product_id) + + +@pytest.fixture +def async_vendor_document_service(async_mpt_vendor, product_id): + return async_mpt_vendor.catalog.products.documents(product_id) diff --git a/tests/e2e/catalog/product/documents/test_async_document.py b/tests/e2e/catalog/product/documents/test_async_document.py new file mode 100644 index 00000000..30fff07f --- /dev/null +++ b/tests/e2e/catalog/product/documents/test_async_document.py @@ -0,0 +1,102 @@ +import pytest + +from mpt_api_client.exceptions import MPTAPIError +from mpt_api_client.rql.query_builder import RQLQuery + +pytestmark = [pytest.mark.flaky, pytest.mark.asyncio] + + +@pytest.fixture +def async_document_service(async_mpt_vendor, product_id): + return async_mpt_vendor.catalog.products.documents(product_id) + + +@pytest.fixture +async def created_document_from_file_async(logger, async_document_service, document_data, pdf_fd): + document_data["documenttype"] = "File" + document = await async_document_service.create(document_data, file=pdf_fd) + yield document + try: + await async_document_service.delete(document.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete document {document.id}: {error.title}") + + +@pytest.fixture +async def created_document_from_link_async(logger, async_document_service, document_data, pdf_url): + document_data["url"] = pdf_url + document = await async_document_service.create(document_data) + yield document + try: + await async_document_service.delete(document.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete document {document.id}: {error.title}") + + +def test_create_document_async(created_document_from_file_async, document_data): + assert created_document_from_file_async.name == document_data["name"] + assert created_document_from_file_async.description == document_data["description"] + + +def test_create_from_link_async(created_document_from_link_async, pdf_url, document_data): + assert created_document_from_link_async.name == document_data["name"] + assert created_document_from_link_async.description == document_data["description"] + + +async def test_update_document_async(async_document_service, created_document_from_file_async): + update_data = {"name": "Updated e2e test document - please delete"} + document = await async_document_service.update(created_document_from_file_async.id, update_data) + assert document.name == update_data["name"] + + +async def test_get_document_async(async_document_service, document_id): + document = await async_document_service.get(document_id) + assert document.id == document_id + + +async def test_download_document_async(async_document_service, document_id): + file_response = await async_document_service.download(document_id) + assert file_response.file_contents is not None + assert file_response.filename == "pdf - empty.pdf" + + +async def test_iterate_documents_async(async_document_service, created_document_from_file_async): + documents = [doc async for doc in async_document_service.iterate()] + assert any(doc.id == created_document_from_file_async.id for doc in documents) + + +async def test_filter_documents_async(async_document_service, created_document_from_file_async): + filtered_service = async_document_service.filter( + RQLQuery(id=created_document_from_file_async.id) + ) + documents = [doc async for doc in filtered_service.iterate()] + assert len(documents) == 1 + assert documents[0].id == created_document_from_file_async.id + + +@pytest.mark.skip(reason="Leaves test documents in published state") +async def test_review_and_publish_document_async( + async_mpt_vendor, async_mpt_ops, created_document_from_file_async, product_id +): + vendor_service = async_mpt_vendor.catalog.products.documents(product_id) + ops_service = async_mpt_ops.catalog.products.documents(product_id) + + document = await vendor_service.review(created_document_from_file_async.id) + assert document.status == "Pending" + + document = await ops_service.publish(created_document_from_file_async.id) + assert document.status == "Published" + + document = await ops_service.unpublish(created_document_from_file_async.id) + assert document.status == "Unpublished" + + +async def test_not_found_async(async_document_service): + with pytest.raises(MPTAPIError): + await async_document_service.get("DOC-000-000-000") + + +async def test_delete_document_async(async_document_service, created_document_from_file_async): + await async_document_service.delete(created_document_from_file_async.id) + with pytest.raises(MPTAPIError): + await async_document_service.get(created_document_from_file_async.id) diff --git a/tests/e2e/catalog/product/documents/test_sync_document.py b/tests/e2e/catalog/product/documents/test_sync_document.py new file mode 100644 index 00000000..828a8994 --- /dev/null +++ b/tests/e2e/catalog/product/documents/test_sync_document.py @@ -0,0 +1,94 @@ +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_document_from_file(logger, vendor_document_service, document_data, pdf_fd): + document_data["documenttype"] = "File" + document = vendor_document_service.create(document_data, pdf_fd) + yield document + try: + vendor_document_service.delete(document.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete document {document.id}: {error.title}") + + +@pytest.fixture +def created_document_from_link(logger, vendor_document_service, document_data, pdf_url): + document_data["url"] = pdf_url + document = vendor_document_service.create(document_data) + yield document + try: + vendor_document_service.delete(document.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete document {document.id}: {error.title}") + + +def test_create_document(created_document_from_file, document_data): + assert created_document_from_file.name == document_data["name"] + assert created_document_from_file.description == document_data["description"] + + +def test_create_from_link(created_document_from_link, pdf_url, document_data): + assert created_document_from_link.name == document_data["name"] + assert created_document_from_link.description == document_data["description"] + + +def test_update_document(vendor_document_service, created_document_from_file): + update_data = {"name": "Updated e2e test document - please delete"} + document = vendor_document_service.update(created_document_from_file.id, update_data) + assert document.name == update_data["name"] + + +def test_get_document(vendor_document_service, document_id): + document = vendor_document_service.get(document_id) + assert document.id == document_id + + +def test_download_document(vendor_document_service, document_id): + file_response = vendor_document_service.download(document_id) + assert file_response.file_contents is not None + assert file_response.filename == "pdf - empty.pdf" + + +def test_iterate_documents(vendor_document_service, created_document_from_file): + documents = list(vendor_document_service.iterate()) + assert any(doc.id == created_document_from_file.id for doc in documents) + + +def test_filter_documents(vendor_document_service, created_document_from_file): + documents = list( + vendor_document_service.filter(RQLQuery(id=created_document_from_file.id)).iterate() + ) + assert len(documents) == 1 + assert documents[0].id == created_document_from_file.id + + +@pytest.mark.skip(reason="Leaves test documents in published state") +def test_review_and_publish_document(mpt_vendor, mpt_ops, created_document_from_file, product_id): + vendor_service = mpt_vendor.catalog.products.documents(product_id) + ops_service = mpt_ops.catalog.products.documents(product_id) + + document = vendor_service.review(created_document_from_file.id) + assert document.status == "Pending" + + document = ops_service.publish(created_document_from_file.id) + assert document.status == "Published" + + document = ops_service.unpublish(created_document_from_file.id) + assert document.status == "Unpublished" + + +def test_not_found(vendor_document_service): + with pytest.raises(MPTAPIError): + vendor_document_service.get("DOC-000-000-000") + + +def test_delete_document(vendor_document_service, created_document_from_file): + vendor_document_service.delete(created_document_from_file.id) + with pytest.raises(MPTAPIError): + vendor_document_service.get(created_document_from_file.id) diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index a6461c4f..ded433dd 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -68,6 +68,17 @@ def project_root_path(): return pathlib.Path(__file__).parent.parent.parent +@pytest.fixture +def pdf_fd(): + icon_path = pathlib.Path(__file__).parent / "empty.pdf" + return pathlib.Path.open(icon_path, "rb") + + +@pytest.fixture +def pdf_url(): + return "https://sample-files.com/downloads/documents/pdf/basic-text.pdf" + + @pytest.fixture def e2e_config(project_root_path): filename = os.getenv("TEST_CONFIG_FILE", "e2e_config.test.json") diff --git a/tests/e2e/empty.pdf b/tests/e2e/empty.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5abbd77a4955ff1b3736f45e405394c832c35750 GIT binary patch literal 3840 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