Skip to content

Commit 2137a1a

Browse files
authored
MPT-14713 E2E TDR Proof of concept (#97)
- Moved unit tests from `tests/` to `tests/unit/` - Added dummy e2e test in `tests/e2e/` - Updated pytest setup to not run tests mark as e2e - Added docker image to run e2e tests with `docker compose run --rm e2e` - Added `pytest-rerunfailures` dev dependency to rerun failed api calls (flaky tests) - Update Error handler to handle JSON responses for generic HTTP Errors (404, 401, ...) - Added e2e to github actions
2 parents ca93d0c + 8e5fbd9 commit 2137a1a

File tree

120 files changed

+167
-43
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

120 files changed

+167
-43
lines changed

.env.sample

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
MPT_API_BASE_URL=https://api...
2+
MPT_API_TOKEN=idt:TKN-...

.github/workflows/pr-build-merge.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,19 @@ jobs:
2626
- name: "Build test containers"
2727
run: docker compose build app_test
2828

29+
- name: "Create environment file"
30+
run: touch .env
31+
2932
- name: "Run validation & test"
3033
run: docker compose run --service-ports app_test
3134

35+
- name: "Run E2E test"
36+
run: docker compose run --service-ports -e MPT_API_BASE_URL=$MPT_API_BASE_URL -e MPT_API_TOKEN=$MPT_API_TOKEN e2e
37+
env:
38+
MPT_API_BASE_URL: ${{ secrets.MPT_API_BASE_URL }}
39+
MPT_API_TOKEN: ${{ secrets.MPT_API_TOKEN }}
40+
41+
3242
- name: "Run SonarCloud Scan"
3343
uses: SonarSource/sonarqube-scan-action@master
3444
env:

docker-compose.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ services:
99
tty: true
1010
volumes:
1111
- .:/mpt_api_client
12+
env_file:
13+
- .env
1214

1315
app_test:
1416
container_name: mpt_api_client_test
@@ -31,6 +33,8 @@ services:
3133
tty: true
3234
volumes:
3335
- .:/mpt_api_client
36+
env_file:
37+
- .env
3438

3539
format:
3640
container_name: mpt_api_client_format
@@ -41,3 +45,15 @@ services:
4145
command: bash -c "ruff check . --select I --fix && ruff format ."
4246
volumes:
4347
- .:/mpt_api_client
48+
49+
e2e:
50+
container_name: mpt_api_client_test
51+
build:
52+
context: .
53+
dockerfile: dev.Dockerfile
54+
working_dir: /mpt_api_client
55+
command: bash -c "pytest -m e2e -p no:randomly --junitxml=e2e-report.xml"
56+
volumes:
57+
- .:/mpt_api_client
58+
env_file:
59+
- .env

mpt_api_client/exceptions.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,27 @@ class MPTError(Exception):
1111
class MPTHttpError(MPTError):
1212
"""Represents an HTTP error."""
1313

14-
def __init__(self, status_code: int, text: str):
14+
def __init__(self, status_code: int, message: str, body: str):
1515
self.status_code = status_code
16-
self.text = text
17-
super().__init__(f"{self.status_code} - {self.text}")
16+
self.body = body
17+
super().__init__(f"HTTP {status_code}: {message}")
1818

1919

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

23-
def __init__(self, status_code: int, payload: dict[str, str]):
24-
super().__init__(status_code, json.dumps(payload))
23+
def __init__(self, status_code: int, message: str, payload: dict[str, str]):
24+
super().__init__(status_code, message, json.dumps(payload))
2525
self.payload = payload
26-
self.status: str | None = payload.get("status")
27-
self.title: str | None = payload.get("title")
28-
self.detail: str | None = payload.get("detail")
26+
self.status: str | None = payload.get("status") or payload.get("statusCode")
27+
self.title: str | None = payload.get("title") or payload.get("message")
28+
self.detail: str | None = payload.get("detail") or message
2929
self.trace_id: str | None = payload.get("traceId")
3030
self.errors: str | None = payload.get("errors")
3131

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

3636
if self.errors:
3737
return f"{base}\n{json.dumps(self.errors, indent=2)}"
@@ -57,11 +57,13 @@ def transform_http_status_exception(http_status_exception: HTTPStatusError) -> M
5757
try:
5858
return MPTAPIError(
5959
status_code=http_status_exception.response.status_code,
60+
message=http_status_exception.args[0],
6061
payload=http_status_exception.response.json(),
6162
)
6263
except json.JSONDecodeError:
63-
payload = http_status_exception.response.content.decode()
64+
body = http_status_exception.response.content.decode()
6465
return MPTHttpError(
6566
status_code=http_status_exception.response.status_code,
66-
text=payload,
67+
message=http_status_exception.args[0],
68+
body=body,
6769
)

pyproject.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ dev = [
3737
"pytest-deadfixtures==2.2.*",
3838
"pytest-mock==3.14.*",
3939
"pytest-randomly==3.16.*",
40+
"pytest-rerunfailures>=16.1",
4041
"pytest-xdist==3.6.*",
4142
"responses==0.25.*",
4243
"respx==0.22.*",
43-
"ruff==0.12.11", # force ruff version to have same formatting everywhere
44+
"ruff==0.12.11", # force ruff version to have same formatting everywhere
4445
"typing-extensions==4.13.*",
4546
"wemake-python-styleguide==1.3.*",
4647
]
@@ -58,13 +59,17 @@ build-backend = "hatchling.build"
5859
[tool.pytest.ini_options]
5960
testpaths = "tests"
6061
pythonpath = "."
61-
addopts = "--cov=mpt_api_client --cov-report=term-missing --cov-report=html --cov-report=xml --import-mode=importlib"
62+
addopts = "--cov=mpt_api_client --cov-report=term-missing --cov-report=html --cov-report=xml --import-mode=importlib -m 'not e2e'"
6263
log_cli = false
6364
asyncio_mode = "auto"
65+
asyncio_default_fixture_loop_scope = "function"
6466
filterwarnings = [
6567
"ignore:Support for class-based `config` is deprecated:DeprecationWarning",
6668
"ignore:pkg_resources is deprecated as an API:DeprecationWarning",
6769
]
70+
markers = [
71+
"e2e: marks tests as e2e"
72+
]
6873

6974
[tool.coverage.run]
7075
branch = true

setup.cfg

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,15 @@ per-file-ignores =
4040
mpt_api_client/resources/catalog/*.py: WPS110 WPS214 WPS215
4141
mpt_api_client/resources/catalog/products.py: WPS204 WPS214 WPS215
4242
mpt_api_client/rql/query_builder.py: WPS110 WPS115 WPS210 WPS214
43-
tests/http/test_async_service.py: WPS204 WPS202
44-
tests/http/test_service.py: WPS204 WPS202
45-
tests/http/test_mixins.py: WPS204 WPS202
46-
tests/resources/catalog/test_products.py: WPS202 WPS210
47-
tests/resources/*/test_mixins.py: WPS118 WPS202 WPS204 WPS235
48-
tests/resources/accounts/test_users.py: WPS204 WPS202 WPS210
49-
tests/test_mpt_client.py: WPS235
50-
51-
tests/*:
43+
tests/unit/http/test_async_service.py: WPS204 WPS202
44+
tests/unit/http/test_service.py: WPS204 WPS202
45+
tests/unit/http/test_mixins.py: WPS204 WPS202
46+
tests/unit/resources/catalog/test_products.py: WPS202 WPS210
47+
tests/unit/resources/*/test_mixins.py: WPS118 WPS202 WPS204 WPS235
48+
tests/unit/resources/accounts/test_users.py: WPS204 WPS202 WPS210
49+
tests/unit/test_mpt_client.py: WPS235
50+
51+
tests/unit/*:
5252
# Allow magic strings.
5353
WPS432
5454
# Found too many modules members.

sonar-project.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ sonar.inclusions=mpt_api_client/**
99
sonar.exclusions=tests/**,**/__init__.py
1010

1111
sonar.python.coverage.reportPaths=coverage.xml
12-
sonar.python.xunit.reportPath=coverage.xml
12+
sonar.python.xunit.reportPath=coverage.xml,e2e-report.xml
1313
sonar.python.version=3
File renamed without changes.

tests/e2e/conftest.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import os
2+
3+
import pytest
4+
5+
from mpt_api_client import MPTClient
6+
7+
8+
@pytest.fixture
9+
def api_token():
10+
return os.getenv("MPT_API_TOKEN")
11+
12+
13+
@pytest.fixture
14+
def base_url():
15+
return os.getenv("MPT_API_BASE_URL")
16+
17+
18+
@pytest.fixture
19+
def mpt_client(api_token, base_url):
20+
return MPTClient.from_config(api_token=api_token, base_url=base_url)

tests/e2e/test_e2e.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import random
2+
3+
import pytest
4+
5+
from mpt_api_client import MPTClient
6+
from mpt_api_client.exceptions import MPTAPIError
7+
8+
9+
@pytest.mark.flaky(reruns=5, reruns_delay=0.01) # noqa: WPS432
10+
@pytest.mark.e2e
11+
def test_example():
12+
assert random.choice([True, False]) # noqa: S311
13+
14+
15+
@pytest.mark.flaky
16+
@pytest.mark.e2e
17+
def test_unauthorised(base_url):
18+
client = MPTClient.from_config(api_token="TKN-invalid", base_url=base_url) # noqa: S106
19+
20+
with pytest.raises(MPTAPIError, match=r"401 Unauthorized"):
21+
client.catalog.products.fetch_page()
22+
23+
24+
@pytest.mark.flaky
25+
@pytest.mark.e2e
26+
def test_access(mpt_client):
27+
product = mpt_client.catalog.products.get("PRD-1975-5250")
28+
assert product.id == "PRD-1975-5250"
29+
assert product.name == "Amazon Web Services"

0 commit comments

Comments
 (0)