diff --git a/.github/workflows/pr-build-merge.yml b/.github/workflows/pr-build-merge.yml index 61a2050c..aa18edaf 100644 --- a/.github/workflows/pr-build-merge.yml +++ b/.github/workflows/pr-build-merge.yml @@ -33,18 +33,19 @@ jobs: RP_API_KEY: ${{ secrets.RP_API_KEY }} MPT_API_BASE_URL: ${{ secrets.MPT_API_BASE_URL }} MPT_API_TOKEN: ${{ secrets.MPT_API_TOKEN }} + MPT_API_TOKEN_CLIENT: ${{ secrets.MPT_API_TOKEN_CLIENT }} + MPT_API_TOKEN_OPERATIONS: ${{ secrets.MPT_API_TOKEN_OPERATIONS }} + MPT_API_TOKEN_VENDOR: ${{ secrets.MPT_API_TOKEN_VENDOR }} - name: "Run validation & test" run: docker compose run --service-ports app_test - name: "Run E2E test" - run: docker compose run --service-ports app_test bash -c "pytest -p no:randomly --no-cov --reportportal --rp-launch=$RP_LAUNCH --rp-api-key=$RP_API_KEY --rp-endpoint=$RP_ENDPOINT --junitxml=e2e-report.xml tests/e2e" + run: docker compose run --service-ports app_test bash -c "pytest -v -p no:randomly --no-cov --reportportal --rp-launch=$RP_LAUNCH --rp-api-key=$RP_API_KEY --rp-endpoint=$RP_ENDPOINT --junitxml=e2e-report.xml tests/e2e" env: RP_LAUNCH: github-e2e-test RP_ENDPOINT: ${{ secrets.RP_ENDPOINT }} RP_API_KEY: ${{ secrets.RP_API_KEY }} - MPT_API_BASE_URL: ${{ secrets.MPT_API_BASE_URL }} - MPT_API_TOKEN: ${{ secrets.MPT_API_TOKEN }} - name: "Run SonarCloud Scan" diff --git a/e2e_config.test.json b/e2e_config.test.json new file mode 100644 index 00000000..c9bcf67c --- /dev/null +++ b/e2e_config.test.json @@ -0,0 +1,3 @@ +{ + "catalog.product.id": "PRD-7255-3950" +} diff --git a/pyproject.toml b/pyproject.toml index c4910e19..ee4ebbaa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dev = [ "mypy==1.15.*", "pre-commit==4.2.*", "pytest==8.3.*", - "pytest-asyncio==1.1.*", + "pytest-asyncio==1.2.*", "pytest-cov==6.1.*", "pytest-deadfixtures==2.2.*", "pytest-mock==3.14.*", diff --git a/seed/catalog/product.py b/seed/catalog/product.py index e4a8c2ce..2f721845 100644 --- a/seed/catalog/product.py +++ b/seed/catalog/product.py @@ -48,7 +48,7 @@ async def init_product( logger.debug("Creating product ...") with pathlib.Path.open(icon, "rb") as icon_file: product = await mpt_vendor.catalog.products.create( - {"name": "Test Product", "website": "https://www.example.com"}, icon=icon_file + {"name": "E2E Seeded", "website": "https://www.example.com"}, icon=icon_file ) context.set_resource(namespace, product) context[f"{namespace}.id"] = product.id diff --git a/tests/e2e/catalog/product/conftest.py b/tests/e2e/catalog/product/conftest.py new file mode 100644 index 00000000..7ede4057 --- /dev/null +++ b/tests/e2e/catalog/product/conftest.py @@ -0,0 +1,13 @@ +import pathlib + +import pytest + + +@pytest.fixture +def product_icon(): + return pathlib.Path.open(pathlib.Path(__file__).parent / "logo.png", "rb") + + +@pytest.fixture +def product_data(): + return {"name": "Test Product", "website": "https://www.example.com"} diff --git a/tests/e2e/catalog/product/logo.png b/tests/e2e/catalog/product/logo.png new file mode 100644 index 00000000..ab9b8cfa Binary files /dev/null and b/tests/e2e/catalog/product/logo.png differ diff --git a/tests/e2e/catalog/product/test_async_product.py b/tests/e2e/catalog/product/test_async_product.py new file mode 100644 index 00000000..af05d52c --- /dev/null +++ b/tests/e2e/catalog/product/test_async_product.py @@ -0,0 +1,63 @@ +import pytest + +from mpt_api_client import RQLQuery +from mpt_api_client.exceptions import MPTAPIError + + +@pytest.fixture +async def async_created_product(logger, async_mpt_vendor, product_data, product_icon): + product = await async_mpt_vendor.catalog.products.create(product_data, icon=product_icon) + + yield product + + try: + await async_mpt_vendor.catalog.products.delete(product.id) + except MPTAPIError as error: + logger.exception("TEARDOWN - Unable to delete product %s: %s", product.id, error.title) + + +@pytest.mark.flaky +def test_create_product(async_created_product, product_data): + assert async_created_product.name == product_data["name"] + + +@pytest.mark.flaky +async def test_update_product(async_mpt_vendor, async_created_product): + update_data = {"name": "Updated Product"} + + product = await async_mpt_vendor.catalog.products.update(async_created_product.id, update_data) + + assert product.name == update_data["name"] + + +@pytest.mark.skip(reason="Leaves test products in the catalog") +@pytest.mark.flaky +async def test_product_review_and_publish(async_mpt_vendor, async_mpt_ops, async_created_product): + await async_mpt_vendor.catalog.products.review(async_created_product.id) + await async_mpt_ops.catalog.products.publish(async_created_product.id) + + +@pytest.mark.flaky +async def test_get_product(async_mpt_vendor, product_id, logger): + await async_mpt_vendor.catalog.products.get(product_id) + + +@pytest.mark.flaky +async def test_product_save_settings(async_mpt_vendor, async_created_product): + await async_mpt_vendor.catalog.products.update_settings( + async_created_product.id, {"itemSelection": True} + ) + + +@pytest.mark.flaky +async def test_filter_and_select_products(async_mpt_vendor, product_id): + select_fields = ["-icon", "-revision", "-settings", "-vendor", "-statistics", "-website"] + + filtered_products = ( + async_mpt_vendor.catalog.products.filter(RQLQuery(id=product_id)) + .filter(RQLQuery(name="E2E Seeded")) + .select(*select_fields) + ) + + products = [product async for product in filtered_products.iterate()] + assert len(products) == 1 diff --git a/tests/e2e/catalog/product/test_sync_product.py b/tests/e2e/catalog/product/test_sync_product.py new file mode 100644 index 00000000..3f793ff9 --- /dev/null +++ b/tests/e2e/catalog/product/test_sync_product.py @@ -0,0 +1,61 @@ +import pytest + +from mpt_api_client import RQLQuery +from mpt_api_client.exceptions import MPTAPIError + + +@pytest.fixture +def created_product(logger, mpt_vendor, product_data, product_icon): + product = mpt_vendor.catalog.products.create(product_data, icon=product_icon) + + yield product + + try: + mpt_vendor.catalog.products.delete(product.id) + except MPTAPIError as error: + logger.exception("TEARDOWN - Unable to delete product %s: %s", product.id, error.title) + + +@pytest.mark.flaky +def test_create_product(created_product, product_data): + assert created_product.name == product_data["name"] + + +@pytest.mark.flaky +def test_update_product(mpt_vendor, created_product): + update_data = {"name": "Updated Product"} + + product = mpt_vendor.catalog.products.update(created_product.id, update_data) + + assert product.name == update_data["name"] + + +@pytest.mark.skip(reason="Leaves test products in the catalog") +@pytest.mark.flaky +def test_product_review_and_publish(mpt_vendor, mpt_ops, created_product): + mpt_vendor.catalog.products.review(created_product.id) + mpt_ops.catalog.products.publish(created_product.id) + + +@pytest.mark.flaky +def test_get_product(mpt_vendor, product_id): + mpt_vendor.catalog.products.get(product_id) + + +@pytest.mark.flaky +def test_product_save_settings(mpt_vendor, created_product): + mpt_vendor.catalog.products.update_settings(created_product.id, {"itemSelection": True}) + + +@pytest.mark.flaky +def test_filter_and_select_products(mpt_vendor, product_id): + select_fields = ["-icon", "-revision", "-settings", "-vendor", "-statistics", "-website"] + + filtered_products = ( + mpt_vendor.catalog.products.filter(RQLQuery(id=product_id)) + .filter(RQLQuery(name="E2E Seeded")) + .select(*select_fields) + ) + + products = list(filtered_products.iterate()) + assert len(products) == 1 diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 2ef69c1f..08001695 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -1,30 +1,80 @@ +import json import logging import os +import pathlib import pytest from reportportal_client import RPLogger -from mpt_api_client import MPTClient +from mpt_api_client import AsyncMPTClient, MPTClient @pytest.fixture -def api_token(): - return os.getenv("MPT_API_TOKEN") +def base_url(): + return os.getenv("MPT_API_BASE_URL") @pytest.fixture -def base_url(): - return os.getenv("MPT_API_BASE_URL") +def mpt_vendor(base_url): + return MPTClient.from_config(api_token=os.getenv("MPT_API_TOKEN_VENDOR"), base_url=base_url) # type: ignore @pytest.fixture -def mpt_client(api_token, base_url): - return MPTClient.from_config(api_token=api_token, base_url=base_url) +def async_mpt_vendor(base_url): + return AsyncMPTClient.from_config( + api_token=os.getenv("MPT_API_TOKEN_VENDOR"), base_url=base_url + ) # type: ignore + + +@pytest.fixture +def mpt_ops(base_url): + return MPTClient.from_config(api_token=os.getenv("MPT_API_TOKEN_OPERATIONS"), base_url=base_url) # type: ignore + +@pytest.fixture +def async_mpt_ops(base_url): + return AsyncMPTClient.from_config( + api_token=os.getenv("MPT_API_TOKEN_OPERATIONS"), base_url=base_url + ) # type: ignore + + +@pytest.fixture +def mpt_client(base_url): + return MPTClient.from_config(api_token=os.getenv("MPT_API_TOKEN_CLIENT"), base_url=base_url) # type: ignore + + +@pytest.fixture +def async_mpt_client(base_url): + return AsyncMPTClient.from_config( + api_token=os.getenv("MPT_API_TOKEN_CLIENT"), base_url=base_url + ) # type: ignore -@pytest.fixture(scope="session") + +@pytest.fixture def rp_logger(): logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) logging.setLoggerClass(RPLogger) return logger + + +@pytest.fixture +def logger(): + return logging.getLogger("E2E") + + +@pytest.fixture +def project_root_path(): + return pathlib.Path(__file__).parent.parent.parent + + +@pytest.fixture +def e2e_config(project_root_path): + filename = os.getenv("TEST_CONFIG_FILE", "e2e_config.test.json") + file_path = project_root_path.joinpath(filename) + return json.loads(file_path.read_text()) + + +@pytest.fixture +def product_id(e2e_config): + return e2e_config["catalog.product.id"] diff --git a/tests/e2e/test_access.py b/tests/e2e/test_access.py new file mode 100644 index 00000000..cf032e4e --- /dev/null +++ b/tests/e2e/test_access.py @@ -0,0 +1,18 @@ +import pytest + +from mpt_api_client import MPTClient +from mpt_api_client.exceptions import MPTAPIError + + +@pytest.mark.flaky +def test_unauthorised(base_url): + client = MPTClient.from_config(api_token="TKN-invalid", base_url=base_url) # noqa: S106 + + with pytest.raises(MPTAPIError, match=r"401 Unauthorized"): + client.catalog.products.fetch_page() + + +@pytest.mark.flaky +def test_access(mpt_vendor, product_id): + product = mpt_vendor.catalog.products.get(product_id) + assert product.id == product_id diff --git a/tests/e2e/test_e2e.py b/tests/e2e/test_e2e.py deleted file mode 100644 index 3d2da099..00000000 --- a/tests/e2e/test_e2e.py +++ /dev/null @@ -1,28 +0,0 @@ -import random - -import pytest - -from mpt_api_client import MPTClient -from mpt_api_client.exceptions import MPTAPIError - - -@pytest.mark.flaky(reruns=5, reruns_delay=0.01) # noqa: WPS432 -def test_example(rp_logger): - choice = random.choice([True, False]) # noqa: S311 - rp_logger.info("Choice: %s", choice) - assert choice is True - - -@pytest.mark.flaky -def test_unauthorised(base_url): - client = MPTClient.from_config(api_token="TKN-invalid", base_url=base_url) # noqa: S106 - - with pytest.raises(MPTAPIError, match=r"401 Unauthorized"): - client.catalog.products.fetch_page() - - -@pytest.mark.flaky -def test_access(mpt_client): - product = mpt_client.catalog.products.get("PRD-1975-5250") - assert product.id == "PRD-1975-5250" - assert product.name == "Amazon Web Services" diff --git a/uv.lock b/uv.lock index ad7ba5f2..331344cb 100644 --- a/uv.lock +++ b/uv.lock @@ -650,7 +650,7 @@ dev = [ { name = "mypy", specifier = "==1.15.*" }, { name = "pre-commit", specifier = "==4.2.*" }, { name = "pytest", specifier = "==8.3.*" }, - { name = "pytest-asyncio", specifier = "==1.1.*" }, + { name = "pytest-asyncio", specifier = "==1.2.*" }, { name = "pytest-cov", specifier = "==6.1.*" }, { name = "pytest-deadfixtures", specifier = "==2.2.*" }, { name = "pytest-mock", specifier = "==3.14.*" }, @@ -1029,14 +1029,15 @@ wheels = [ [[package]] name = "pytest-asyncio" -version = "1.1.0" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4e/51/f8794af39eeb870e87a8c8068642fc07bce0c854d6865d7dd0f2a9d338c2/pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea", size = 46652, upload-time = "2025-07-16T04:29:26.393Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157, upload-time = "2025-07-16T04:29:24.929Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, ] [[package]]