Skip to content

Commit a6ca5c6

Browse files
committed
MPT-14713 E2E TDR Proof of concept
- 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, ...)
1 parent 7dd3444 commit a6ca5c6

File tree

7 files changed

+78
-22
lines changed

7 files changed

+78
-22
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-...

docker-compose.yml

Lines changed: 6 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
@@ -51,3 +55,5 @@ services:
5155
command: bash -c "pytest -m e2e -p no:randomly --junitxml=e2e-report.xml"
5256
volumes:
5357
- .:/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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ pythonpath = "."
6262
addopts = "--cov=mpt_api_client --cov-report=term-missing --cov-report=html --cov-report=xml --import-mode=importlib -m 'not e2e'"
6363
log_cli = false
6464
asyncio_mode = "auto"
65+
asyncio_default_fixture_loop_scope = "function"
6566
filterwarnings = [
6667
"ignore:Support for class-based `config` is deprecated:DeprecationWarning",
6768
"ignore:pkg_resources is deprecated as an API:DeprecationWarning",

tests/e2e/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77

88
@pytest.fixture
99
def api_token():
10-
return os.getenv("API_TOKEN")
10+
return os.getenv("MPT_API_TOKEN")
1111

1212

1313
@pytest.fixture
1414
def base_url():
15-
return os.getenv("API_URL")
15+
return os.getenv("MPT_API_BASE_URL")
1616

1717

1818
@pytest.fixture

tests/e2e/test_e2e.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,28 @@
22

33
import pytest
44

5+
from mpt_api_client import MPTClient
6+
from mpt_api_client.exceptions import MPTAPIError
7+
58

69
@pytest.mark.flaky(reruns=5, reruns_delay=0.01) # noqa: WPS432
710
@pytest.mark.e2e
811
def test_example():
912
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"

tests/unit/test_exceptions.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,34 @@
1010

1111

1212
def test_http_error():
13-
exception = MPTHttpError(status_code=400, text="Content")
13+
exception = MPTHttpError(status_code=400, message="Bad request", body="Content")
1414

1515
assert exception.status_code == 400
16-
assert exception.text == "Content"
16+
assert exception.body == "Content"
17+
assert str(exception) == "HTTP 400: Bad request"
18+
19+
20+
def test_http_error_not_found_from_mpt(): # noqa: WPS218
21+
status_code = 400 # changed from 404 for testing purposes
22+
api_status_code = 404
23+
payload = {"message": "Resource not found", "statusCode": api_status_code}
24+
message = (
25+
"Client error '404 Resource Not Found' for url "
26+
"'https://api.s1.show/public/public/v1/catalog/products?limit=100&offset=0'\n"
27+
"For more information check: "
28+
"https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404"
29+
)
30+
31+
exception = MPTAPIError(status_code=status_code, message=message, payload=payload)
32+
33+
assert exception.status_code == status_code
34+
assert exception.payload == payload
35+
assert exception.status == api_status_code
36+
assert exception.title == "Resource not found"
37+
assert exception.detail == message
38+
assert exception.trace_id is None
39+
assert exception.errors is None
40+
assert str(exception) == f"404 Resource not found - {message} (no-trace-id)"
1741

1842

1943
def test_api_error(): # noqa: WPS218
@@ -24,7 +48,7 @@ def test_api_error(): # noqa: WPS218
2448
"traceId": "abc123",
2549
"errors": "Some error details",
2650
}
27-
exception = MPTAPIError(status_code=400, payload=payload)
51+
exception = MPTAPIError(status_code=400, message="Bad Request", payload=payload)
2852

2953
assert exception.status_code == 400
3054
assert exception.payload == payload
@@ -43,7 +67,7 @@ def test_api_error_str_and_repr():
4367
"traceId": "abc123",
4468
"errors": "Some error details",
4569
}
46-
exception = MPTAPIError(status_code=400, payload=payload)
70+
exception = MPTAPIError(status_code=400, message="Bad request", payload=payload)
4771

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

63-
exception = MPTAPIError(status_code=400, payload=payload)
87+
exception = MPTAPIError(status_code=400, message="Bad request", payload=payload)
6488

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

6791

68-
def test_transform_http_status_exception():
92+
def test_transform_http_status_exception_api():
6993
payload = {
7094
"status": "400",
7195
"title": "Bad Request",
@@ -88,17 +112,18 @@ def test_transform_http_status_exception():
88112
assert err.payload == payload
89113

90114

91-
def test_transform_http_status_exception_json():
115+
def test_transform_http_status_exception():
92116
response = Response(
93117
status_code=500,
94118
request=Request("GET", "http://test"),
95119
content=b"Internal Server Error",
96120
headers={"content-type": "text/plain"},
97121
)
98-
exc = HTTPStatusError("error", request=response.request, response=response)
122+
exc = HTTPStatusError("Error message", request=response.request, response=response)
99123

100124
err = transform_http_status_exception(exc)
101125

102126
assert isinstance(err, MPTHttpError)
103127
assert err.status_code == 500
104-
assert err.text == "Internal Server Error"
128+
assert err.body == "Internal Server Error"
129+
assert str(err) == "HTTP 500: Error message"

0 commit comments

Comments
 (0)