From a6e063b32071c2aac4c73eb8fdc88637f62f02c1 Mon Sep 17 00:00:00 2001 From: Robert Segal Date: Wed, 26 Nov 2025 19:42:41 -0700 Subject: [PATCH] Seeded and created e2e tests for commerce agreements --- e2e_config.test.json | 7 ++ .../resources/commerce/agreements.py | 24 ++++++ pyproject.toml | 1 + tests/e2e/accounts/conftest.py | 15 ---- tests/e2e/accounts/licensees/conftest.py | 5 -- tests/e2e/commerce/agreement/conftest.py | 51 +++++++++++++ .../agreement/test_async_agreement.py | 76 +++++++++++++++++++ .../commerce/agreement/test_sync_agreement.py | 74 ++++++++++++++++++ tests/e2e/commerce/conftest.py | 11 +++ tests/e2e/conftest.py | 25 ++++++ tests/seed/test_seed_api.py | 2 + .../resources/commerce/test_agreements.py | 42 ++++++++++ uv.lock | 13 +++- 13 files changed, 325 insertions(+), 21 deletions(-) create mode 100644 tests/e2e/commerce/agreement/conftest.py create mode 100644 tests/e2e/commerce/agreement/test_async_agreement.py create mode 100644 tests/e2e/commerce/agreement/test_sync_agreement.py create mode 100644 tests/e2e/commerce/conftest.py diff --git a/e2e_config.test.json b/e2e_config.test.json index df53b3bc..c04a14b1 100644 --- a/e2e_config.test.json +++ b/e2e_config.test.json @@ -11,6 +11,13 @@ "accounts.seller.id": "SEL-7310-3075", "accounts.user.id": "USR-9673-3314", "accounts.user_group.id": "UGR-6822-0561", + "commerce.agreement.id": "AGR-9850-2169-6098", + "commerce.product.id": "PRD-1767-7355", + "commerce.product.item.id": "ITM-1767-7355-0001", + "commerce.product.listing.id": "LST-5489-0806", + "commerce.product.template.id": "TPL-1767-7355-0003", + "commerce.authorization.id": "AUT-0031-2873", + "commerce.client.id": "ACC-1086-6867", "catalog.product.item.id": "ITM-7255-3950-0751", "catalog.product.document.id": "PDC-7255-3950-0001", "catalog.product.id": "PRD-7255-3950", diff --git a/mpt_api_client/resources/commerce/agreements.py b/mpt_api_client/resources/commerce/agreements.py index b1fe0ea9..a9d2b8d7 100644 --- a/mpt_api_client/resources/commerce/agreements.py +++ b/mpt_api_client/resources/commerce/agreements.py @@ -44,6 +44,18 @@ def template(self, agreement_id: str) -> str: response = self._resource_do_request(agreement_id, action="template") return response.text + def render(self, agreement_id: str) -> str: + """Renders the template for the given Agreement id. + + Args: + agreement_id: Agreement ID. + + Returns: + Rendered Agreement. + """ + response = self._resource_do_request(agreement_id, action="render") + return response.text + def attachments(self, agreement_id: str) -> AgreementsAttachmentService: """Get the attachments service for the given Agreement id. @@ -79,6 +91,18 @@ async def template(self, agreement_id: str) -> str: response = await self._resource_do_request(agreement_id, action="template") return response.text + async def render(self, agreement_id: str) -> str: + """Renders the template for the given Agreement id. + + Args: + agreement_id: Agreement ID. + + Returns: + Rendered Agreement. + """ + response = await self._resource_do_request(agreement_id, action="render") + return response.text + def attachments(self, agreement_id: str) -> AsyncAgreementsAttachmentService: """Get the attachments service for the given Agreement id. diff --git a/pyproject.toml b/pyproject.toml index 28464ecb..c12a716f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ dev = [ "ruff==0.12.11", # force ruff version to have same formatting everywhere "typing-extensions==4.13.*", "wemake-python-styleguide==1.3.*", + "types-python-dateutil", ] [tool.hatch.build.targets.sdist] diff --git a/tests/e2e/accounts/conftest.py b/tests/e2e/accounts/conftest.py index aa08c88e..bf3147ba 100644 --- a/tests/e2e/accounts/conftest.py +++ b/tests/e2e/accounts/conftest.py @@ -26,21 +26,6 @@ def currencies(): return ["USD", "EUR"] -@pytest.fixture -def account_id(e2e_config): - return e2e_config["accounts.account.id"] - - -@pytest.fixture -def seller_id(e2e_config): - return e2e_config["accounts.seller.id"] - - -@pytest.fixture -def buyer_id(e2e_config): - return e2e_config["accounts.buyer.id"] - - @pytest.fixture def user_group_id(e2e_config): return e2e_config["accounts.user_group.id"] diff --git a/tests/e2e/accounts/licensees/conftest.py b/tests/e2e/accounts/licensees/conftest.py index 8aa4bebf..c2b4fe7b 100644 --- a/tests/e2e/accounts/licensees/conftest.py +++ b/tests/e2e/accounts/licensees/conftest.py @@ -11,11 +11,6 @@ def licensee_group_id(e2e_config): return e2e_config["accounts.licensee.group.id"] -@pytest.fixture -def licensee_id(e2e_config): - return e2e_config["accounts.licensee.id"] - - @pytest.fixture def invalid_licensee_id(): return "LCE-0000-0000-0000" diff --git a/tests/e2e/commerce/agreement/conftest.py b/tests/e2e/commerce/agreement/conftest.py new file mode 100644 index 00000000..e0b028f9 --- /dev/null +++ b/tests/e2e/commerce/agreement/conftest.py @@ -0,0 +1,51 @@ +import pytest +from freezegun import freeze_time + + +@pytest.fixture +def invalid_agreement_id(): + return "AGR-0000-0000" + + +@pytest.fixture +def agreement_factory( # noqa: WPS211 + account_id, + seller_id, + buyer_id, + licensee_id, + commerce_product_id, + authorization_id, +): + @freeze_time("2025-11-14T09:00:00.000Z") + def factory( + name: str = "E2E Created Agreement", + client_external_id: str = "test-client-external-id", + vendor_external_id: str = "test-vendor-external-id", + ): + return { + "name": name, + "status": "Active", + "vendor": {"id": account_id}, + "authorization": {"id": authorization_id}, + "seller": {"id": seller_id}, + "buyer": {"id": buyer_id}, + "licensee": {"id": licensee_id}, + "product": {"id": commerce_product_id}, + "value": { + "PPxY": 150, + "PPxM": 12.5, + "SPxY": 165, + "SPxM": 13.75, + "markup": 0.1, + "margin": 0.11, + "currency": "USD", + }, + "startDate": "2025-11-14T09:00:00.000Z", + "endDate": "2026-11-13T09:00:00.000Z", + "externalIds": { + "client": client_external_id, + "vendor": vendor_external_id, + }, + } + + return factory diff --git a/tests/e2e/commerce/agreement/test_async_agreement.py b/tests/e2e/commerce/agreement/test_async_agreement.py new file mode 100644 index 00000000..51b90566 --- /dev/null +++ b/tests/e2e/commerce/agreement/test_async_agreement.py @@ -0,0 +1,76 @@ +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_agreement(async_mpt_ops, agreement_factory): + new_agreement_request_data = agreement_factory( + name="E2E Created Agreement", + ) + + return await async_mpt_ops.commerce.agreements.create(new_agreement_request_data) + + +async def test_get_agreement_by_id(async_mpt_ops, agreement_id): + result = await async_mpt_ops.commerce.agreements.get(agreement_id) + + assert result is not None + + +async def test_list_agreements(async_mpt_ops): + limit = 10 + + result = await async_mpt_ops.commerce.agreements.fetch_page(limit=limit) + + assert len(result) > 0 + + +async def test_get_agreement_by_id_not_found(async_mpt_ops, invalid_agreement_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await async_mpt_ops.commerce.agreements.get(invalid_agreement_id) + + +async def test_filter_agreements(async_mpt_ops, agreement_id): + select_fields = ["-value"] + filtered_agreements = ( + async_mpt_ops.commerce.agreements.filter(RQLQuery(id=agreement_id)) + .filter(RQLQuery(name="E2E Seeded For Commerce for Test Api Client Client")) + .select(*select_fields) + ) + + result = [filtered_agreement async for filtered_agreement in filtered_agreements.iterate()] + + assert len(result) == 1 + + +def test_create_agreement(created_agreement): + result = created_agreement + + assert result is not None + + +async def test_update_agreement(async_mpt_ops, created_agreement, agreement_factory): + updated_name = "E2E Updated Agreement Name" + updated_agreement_data = agreement_factory(name=updated_name) + + result = await async_mpt_ops.commerce.agreements.update( + created_agreement.id, updated_agreement_data + ) + + assert result is not None + + +async def test_get_agreement_render(async_mpt_ops, agreement_id): + result = await async_mpt_ops.commerce.agreements.render(agreement_id) + + assert result is not None + + +async def test_get_agreement_template(async_mpt_ops, agreement_id): + result = await async_mpt_ops.commerce.agreements.template(agreement_id) + + assert result is not None diff --git a/tests/e2e/commerce/agreement/test_sync_agreement.py b/tests/e2e/commerce/agreement/test_sync_agreement.py new file mode 100644 index 00000000..47c988b9 --- /dev/null +++ b/tests/e2e/commerce/agreement/test_sync_agreement.py @@ -0,0 +1,74 @@ +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_agreement(mpt_ops, agreement_factory): + new_agreement_request_data = agreement_factory( + name="E2E Created Agreement", + ) + + return mpt_ops.commerce.agreements.create(new_agreement_request_data) + + +def test_get_agreement_by_id(mpt_ops, agreement_id): + result = mpt_ops.commerce.agreements.get(agreement_id) + + assert result is not None + + +def test_list_agreements(mpt_ops): + limit = 10 + + result = mpt_ops.commerce.agreements.fetch_page(limit=limit) + + assert len(result) > 0 + + +def test_get_agreement_by_id_not_found(mpt_ops, invalid_agreement_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + mpt_ops.commerce.agreements.get(invalid_agreement_id) + + +def test_filter_agreements(mpt_ops, agreement_id): + select_fields = ["-value"] + filtered_agreements = ( + mpt_ops.commerce.agreements.filter(RQLQuery(id=agreement_id)) + .filter(RQLQuery(name="E2E Seeded For Commerce for Test Api Client Client")) + .select(*select_fields) + ) + + result = list(filtered_agreements.iterate()) + + assert len(result) == 1 + + +def test_create_agreement(created_agreement): + result = created_agreement + + assert result is not None + + +def test_update_agreement(mpt_ops, created_agreement, agreement_factory): + updated_name = "E2E Updated Agreement Name" + updated_agreement_data = agreement_factory(name=updated_name) + + result = mpt_ops.commerce.agreements.update(created_agreement.id, updated_agreement_data) + + assert result is not None + + +def test_get_agreement_render(mpt_ops, agreement_id): + result = mpt_ops.commerce.agreements.render(agreement_id) + + assert result is not None + + +def test_get_agreement_template(mpt_ops, agreement_id): + result = mpt_ops.commerce.agreements.template(agreement_id) + + assert result is not None diff --git a/tests/e2e/commerce/conftest.py b/tests/e2e/commerce/conftest.py new file mode 100644 index 00000000..18693e5e --- /dev/null +++ b/tests/e2e/commerce/conftest.py @@ -0,0 +1,11 @@ +import pytest + + +@pytest.fixture +def agreement_id(e2e_config): + return e2e_config["commerce.agreement.id"] + + +@pytest.fixture +def commerce_product_id(e2e_config): + return e2e_config["commerce.product.id"] diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 0bea87e7..c5912fac 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -116,3 +116,28 @@ def logo_fd(): @pytest.fixture def user_id(e2e_config): return e2e_config["accounts.user.id"] + + +@pytest.fixture +def account_id(e2e_config): + return e2e_config["accounts.account.id"] + + +@pytest.fixture +def seller_id(e2e_config): + return e2e_config["accounts.seller.id"] + + +@pytest.fixture +def buyer_id(e2e_config): + return e2e_config["accounts.buyer.id"] + + +@pytest.fixture +def licensee_id(e2e_config): + return e2e_config["accounts.licensee.id"] + + +@pytest.fixture +def authorization_id(e2e_config): + return e2e_config["commerce.authorization.id"] diff --git a/tests/seed/test_seed_api.py b/tests/seed/test_seed_api.py index a139e32c..d3da50bd 100644 --- a/tests/seed/test_seed_api.py +++ b/tests/seed/test_seed_api.py @@ -23,6 +23,7 @@ def context_file_path(tmp_path): async def test_seed_api_success(mock_context): with ( patch("seed.seed_api.seed_catalog", new_callable=AsyncMock) as mock_seed_catalog, + patch("seed.seed_api.seed_commerce", new_callable=AsyncMock) as mock_seed_commerce, patch("seed.seed_api.context_file") as mock_context_file, patch("seed.seed_api.load_context") as load, patch("seed.seed_api.save_context") as save, @@ -34,4 +35,5 @@ async def test_seed_api_success(mock_context): load.assert_called_once() mock_seed_catalog.assert_called_once() + mock_seed_commerce.assert_called_once() save.assert_called_once() diff --git a/tests/unit/resources/commerce/test_agreements.py b/tests/unit/resources/commerce/test_agreements.py index d0ff100e..0de58f13 100644 --- a/tests/unit/resources/commerce/test_agreements.py +++ b/tests/unit/resources/commerce/test_agreements.py @@ -46,6 +46,48 @@ def test_template(http_client): assert result == "# Order Template\n\nThis is a markdown template." +def test_render(http_client): + agreements_service = AgreementsService(http_client=http_client) + rendered_content = "# Order Template\n\nThis is a markdown template." + with respx.mock: + mock_route = respx.get( + "https://api.example.com/public/v1/commerce/agreements/AGR-123/render" + ).mock( + return_value=httpx.Response( + status_code=200, + headers={"content-type": "text/html"}, + content=rendered_content, + ) + ) + + result = agreements_service.render("AGR-123") + + assert mock_route.called + assert mock_route.call_count == 1 + assert result == rendered_content + + +async def test_async_render(async_http_client): + async_agreements_service = AsyncAgreementsService(http_client=async_http_client) + rendered_content = "# Order Template\n\nThis is a markdown template." + with respx.mock: + mock_route = respx.get( + "https://api.example.com/public/v1/commerce/agreements/AGR-123/render" + ).mock( + return_value=httpx.Response( + status_code=200, + headers={"content-type": "text/html"}, + content=rendered_content, + ) + ) + + result = await async_agreements_service.render("AGR-123") + + assert mock_route.called + assert mock_route.call_count == 1 + assert result == rendered_content + + def test_attachments_service(http_client): agreements_service = AgreementsService(http_client=http_client) diff --git a/uv.lock b/uv.lock index 0e67ab32..e3d1d10a 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.12, <4" [[package]] @@ -657,6 +657,7 @@ dev = [ { name = "responses" }, { name = "respx" }, { name = "ruff" }, + { name = "types-python-dateutil" }, { name = "typing-extensions" }, { name = "wemake-python-styleguide" }, ] @@ -689,6 +690,7 @@ dev = [ { name = "responses", specifier = "==0.25.*" }, { name = "respx", specifier = "==0.22.*" }, { name = "ruff", specifier = "==0.12.11" }, + { name = "types-python-dateutil" }, { name = "typing-extensions", specifier = "==4.13.*" }, { name = "wemake-python-styleguide", specifier = "==1.3.*" }, ] @@ -1334,6 +1336,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20251115" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/36/06d01fb52c0d57e9ad0c237654990920fa41195e4b3d640830dabf9eeb2f/types_python_dateutil-2.9.0.20251115.tar.gz", hash = "sha256:8a47f2c3920f52a994056b8786309b43143faa5a64d4cbb2722d6addabdf1a58", size = 16363, upload-time = "2025-11-15T03:00:13.717Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/0b/56961d3ba517ed0df9b3a27bfda6514f3d01b28d499d1bce9068cfe4edd1/types_python_dateutil-2.9.0.20251115-py3-none-any.whl", hash = "sha256:9cf9c1c582019753b8639a081deefd7e044b9fa36bd8217f565c6c4e36ee0624", size = 18251, upload-time = "2025-11-15T03:00:12.317Z" }, +] + [[package]] name = "typing-extensions" version = "4.13.2"