Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
MPT_API_BASE_URL=https://api...
MPT_API_TOKEN=idt:TKN-...
10 changes: 10 additions & 0 deletions .github/workflows/pr-build-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,19 @@ jobs:
- name: "Build test containers"
run: docker compose build app_test

- name: "Create environment file"
run: touch .env

- name: "Run validation & test"
run: docker compose run --service-ports app_test

- name: "Run E2E test"
run: docker compose run --service-ports -e MPT_API_BASE_URL=$MPT_API_BASE_URL -e MPT_API_TOKEN=$MPT_API_TOKEN e2e
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep in mind that the values of these parameters depend on the branch for main --> test env/ for release/* - staging env

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  env:
        MPT_API_BASE_URL: ${{ secrets.MPT_API_BASE_URL }}
        MPT_API_TOKEN: ${{ secrets.MPT_API_TOKEN }}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we may have to review this part. But so far we can control those variables from the pipeline setup.

env:
MPT_API_BASE_URL: ${{ secrets.MPT_API_BASE_URL }}
MPT_API_TOKEN: ${{ secrets.MPT_API_TOKEN }}


- name: "Run SonarCloud Scan"
uses: SonarSource/sonarqube-scan-action@master
env:
Expand Down
16 changes: 16 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ services:
tty: true
volumes:
- .:/mpt_api_client
env_file:
- .env

app_test:
container_name: mpt_api_client_test
Expand All @@ -31,6 +33,8 @@ services:
tty: true
volumes:
- .:/mpt_api_client
env_file:
- .env

format:
container_name: mpt_api_client_format
Expand All @@ -41,3 +45,15 @@ services:
command: bash -c "ruff check . --select I --fix && ruff format ."
volumes:
- .:/mpt_api_client

e2e:
container_name: mpt_api_client_test
build:
context: .
dockerfile: dev.Dockerfile
working_dir: /mpt_api_client
command: bash -c "pytest -m e2e -p no:randomly --junitxml=e2e-report.xml"
volumes:
- .:/mpt_api_client
env_file:
- .env
24 changes: 13 additions & 11 deletions mpt_api_client/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,27 @@ class MPTError(Exception):
class MPTHttpError(MPTError):
"""Represents an HTTP error."""

def __init__(self, status_code: int, text: str):
def __init__(self, status_code: int, message: str, body: str):
self.status_code = status_code
self.text = text
super().__init__(f"{self.status_code} - {self.text}")
self.body = body
super().__init__(f"HTTP {status_code}: {message}")


class MPTAPIError(MPTHttpError):
"""Represents an API error."""

def __init__(self, status_code: int, payload: dict[str, str]):
super().__init__(status_code, json.dumps(payload))
def __init__(self, status_code: int, message: str, payload: dict[str, str]):
super().__init__(status_code, message, json.dumps(payload))
self.payload = payload
self.status: str | None = payload.get("status")
self.title: str | None = payload.get("title")
self.detail: str | None = payload.get("detail")
self.status: str | None = payload.get("status") or payload.get("statusCode")
self.title: str | None = payload.get("title") or payload.get("message")
self.detail: str | None = payload.get("detail") or message
self.trace_id: str | None = payload.get("traceId")
self.errors: str | None = payload.get("errors")

@override
def __str__(self) -> str:
base = f"{self.status} {self.title} - {self.detail} ({self.trace_id})"
base = f"{self.status} {self.title} - {self.detail} ({self.trace_id or 'no-trace-id'})" # noqa: WPS221 WPS237

if self.errors:
return f"{base}\n{json.dumps(self.errors, indent=2)}"
Expand All @@ -57,11 +57,13 @@ def transform_http_status_exception(http_status_exception: HTTPStatusError) -> M
try:
return MPTAPIError(
status_code=http_status_exception.response.status_code,
message=http_status_exception.args[0],
payload=http_status_exception.response.json(),
)
except json.JSONDecodeError:
payload = http_status_exception.response.content.decode()
body = http_status_exception.response.content.decode()
return MPTHttpError(
status_code=http_status_exception.response.status_code,
text=payload,
message=http_status_exception.args[0],
body=body,
)
9 changes: 7 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ dev = [
"pytest-deadfixtures==2.2.*",
"pytest-mock==3.14.*",
"pytest-randomly==3.16.*",
"pytest-rerunfailures>=16.1",
"pytest-xdist==3.6.*",
"responses==0.25.*",
"respx==0.22.*",
"ruff==0.12.11", # force ruff version to have same formatting everywhere
"ruff==0.12.11", # force ruff version to have same formatting everywhere
"typing-extensions==4.13.*",
"wemake-python-styleguide==1.3.*",
]
Expand All @@ -58,13 +59,17 @@ build-backend = "hatchling.build"
[tool.pytest.ini_options]
testpaths = "tests"
pythonpath = "."
addopts = "--cov=mpt_api_client --cov-report=term-missing --cov-report=html --cov-report=xml --import-mode=importlib"
addopts = "--cov=mpt_api_client --cov-report=term-missing --cov-report=html --cov-report=xml --import-mode=importlib -m 'not e2e'"
log_cli = false
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
filterwarnings = [
"ignore:Support for class-based `config` is deprecated:DeprecationWarning",
"ignore:pkg_resources is deprecated as an API:DeprecationWarning",
]
markers = [
"e2e: marks tests as e2e"
]

[tool.coverage.run]
branch = true
Expand Down
18 changes: 9 additions & 9 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ per-file-ignores =
mpt_api_client/resources/catalog/*.py: WPS110 WPS214 WPS215
mpt_api_client/resources/catalog/products.py: WPS204 WPS214 WPS215
mpt_api_client/rql/query_builder.py: WPS110 WPS115 WPS210 WPS214
tests/http/test_async_service.py: WPS204 WPS202
tests/http/test_service.py: WPS204 WPS202
tests/http/test_mixins.py: WPS204 WPS202
tests/resources/catalog/test_products.py: WPS202 WPS210
tests/resources/*/test_mixins.py: WPS118 WPS202 WPS204 WPS235
tests/resources/accounts/test_users.py: WPS204 WPS202 WPS210
tests/test_mpt_client.py: WPS235

tests/*:
tests/unit/http/test_async_service.py: WPS204 WPS202
tests/unit/http/test_service.py: WPS204 WPS202
tests/unit/http/test_mixins.py: WPS204 WPS202
tests/unit/resources/catalog/test_products.py: WPS202 WPS210
tests/unit/resources/*/test_mixins.py: WPS118 WPS202 WPS204 WPS235
tests/unit/resources/accounts/test_users.py: WPS204 WPS202 WPS210
tests/unit/test_mpt_client.py: WPS235

tests/unit/*:
# Allow magic strings.
WPS432
# Found too many modules members.
Expand Down
2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ sonar.inclusions=mpt_api_client/**
sonar.exclusions=tests/**,**/__init__.py

sonar.python.coverage.reportPaths=coverage.xml
sonar.python.xunit.reportPath=coverage.xml
sonar.python.xunit.reportPath=coverage.xml,e2e-report.xml
sonar.python.version=3
File renamed without changes.
20 changes: 20 additions & 0 deletions tests/e2e/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import os

import pytest

from mpt_api_client import MPTClient


@pytest.fixture
def api_token():
return os.getenv("MPT_API_TOKEN")


@pytest.fixture
def base_url():
return os.getenv("MPT_API_BASE_URL")


@pytest.fixture
def mpt_client(api_token, base_url):
return MPTClient.from_config(api_token=api_token, base_url=base_url)
29 changes: 29 additions & 0 deletions tests/e2e/test_e2e.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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
@pytest.mark.e2e
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you remind me what marks should be here? Looks a little bit extra to have e2e folder and e2e mark at the same time

Copy link
Contributor Author

@albertsola albertsola Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I completely agree, we have not taken a final call in which way to go.

Current setup if you run pytest with no parameters, the e2e will not run.

We can cleanup these marks in the future after we do the final call on the TDR.

def test_example():
assert random.choice([True, False]) # noqa: S311


@pytest.mark.flaky
@pytest.mark.e2e
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
@pytest.mark.e2e
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"
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion tests/http/conftest.py → tests/unit/http/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
CollectionMixin,
ManagedResourceMixin,
)
from tests.conftest import DummyModel
from tests.unit.conftest import DummyModel


class DummyService( # noqa: WPS215
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from mpt_api_client.exceptions import MPTError
from mpt_api_client.http.async_client import AsyncHTTPClient
from tests.conftest import API_TOKEN, API_URL
from tests.unit.conftest import API_TOKEN, API_URL


@pytest.fixture
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from mpt_api_client.http import Service
from mpt_api_client.http.query_state import QueryState
from tests.conftest import DummyModel
from tests.http.conftest import DummyService
from tests.unit.conftest import DummyModel
from tests.unit.http.conftest import DummyService


class ParametrisedDummyService( # noqa: WPS215
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from mpt_api_client.exceptions import MPTError
from mpt_api_client.http.client import HTTPClient
from tests.conftest import API_TOKEN, API_URL
from tests.unit.conftest import API_TOKEN, API_URL


def test_http_initialization(mocker):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
AsyncMediaService,
MediaService,
)
from tests.conftest import DummyModel
from tests.unit.conftest import DummyModel


@pytest.fixture
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest

from mpt_api_client.models import Collection
from tests.conftest import DummyModel
from tests.unit.conftest import DummyModel


@pytest.fixture
Expand Down
File renamed without changes.
Empty file.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
InvitableMixin,
ValidateMixin,
)
from tests.conftest import DummyModel
from tests.unit.conftest import DummyModel


class DummyActivatableService(
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
RecalculatableMixin,
RegeneratableMixin,
)
from tests.conftest import DummyModel
from tests.unit.conftest import DummyModel


class DummyRegeneratableService(
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
AsyncPublishableMixin,
PublishableMixin,
)
from tests.conftest import DummyModel
from tests.unit.conftest import DummyModel


class DummyPublishableService( # noqa: WPS215
Expand Down
Empty file.
Empty file.
Empty file added tests/unit/rql/__init__.py
Empty file.
Empty file.
43 changes: 34 additions & 9 deletions tests/test_exceptions.py → tests/unit/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,34 @@


def test_http_error():
exception = MPTHttpError(status_code=400, text="Content")
exception = MPTHttpError(status_code=400, message="Bad request", body="Content")

assert exception.status_code == 400
assert exception.text == "Content"
assert exception.body == "Content"
assert str(exception) == "HTTP 400: Bad request"


def test_http_error_not_found_from_mpt(): # noqa: WPS218
status_code = 400 # changed from 404 for testing purposes
api_status_code = 404
payload = {"message": "Resource not found", "statusCode": api_status_code}
message = (
"Client error '404 Resource Not Found' for url "
"'https://api.s1.show/public/public/v1/catalog/products?limit=100&offset=0'\n"
"For more information check: "
"https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404"
)

exception = MPTAPIError(status_code=status_code, message=message, payload=payload)

assert exception.status_code == status_code
assert exception.payload == payload
assert exception.status == api_status_code
assert exception.title == "Resource not found"
assert exception.detail == message
assert exception.trace_id is None
assert exception.errors is None
assert str(exception) == f"404 Resource not found - {message} (no-trace-id)"


def test_api_error(): # noqa: WPS218
Expand All @@ -24,7 +48,7 @@ def test_api_error(): # noqa: WPS218
"traceId": "abc123",
"errors": "Some error details",
}
exception = MPTAPIError(status_code=400, payload=payload)
exception = MPTAPIError(status_code=400, message="Bad Request", payload=payload)

assert exception.status_code == 400
assert exception.payload == payload
Expand All @@ -43,7 +67,7 @@ def test_api_error_str_and_repr():
"traceId": "abc123",
"errors": "Some error details",
}
exception = MPTAPIError(status_code=400, payload=payload)
exception = MPTAPIError(status_code=400, message="Bad request", payload=payload)

assert str(exception) == '400 Bad Request - Invalid input (abc123)\n"Some error details"'
assert repr(exception) == (
Expand All @@ -60,12 +84,12 @@ def test_api_error_str_no_errors():
"traceId": "abc123",
}

exception = MPTAPIError(status_code=400, payload=payload)
exception = MPTAPIError(status_code=400, message="Bad request", payload=payload)

assert str(exception) == "400 Bad Request - Invalid input (abc123)"


def test_transform_http_status_exception():
def test_transform_http_status_exception_api():
payload = {
"status": "400",
"title": "Bad Request",
Expand All @@ -88,17 +112,18 @@ def test_transform_http_status_exception():
assert err.payload == payload


def test_transform_http_status_exception_json():
def test_transform_http_status_exception():
response = Response(
status_code=500,
request=Request("GET", "http://test"),
content=b"Internal Server Error",
headers={"content-type": "text/plain"},
)
exc = HTTPStatusError("error", request=response.request, response=response)
exc = HTTPStatusError("Error message", request=response.request, response=response)

err = transform_http_status_exception(exc)

assert isinstance(err, MPTHttpError)
assert err.status_code == 500
assert err.text == "Internal Server Error"
assert err.body == "Internal Server Error"
assert str(err) == "HTTP 500: Error message"
Loading