refactor: cleanup

This commit is contained in:
zhom
2026-09-09 10:09:14 +04:00
parent 598d3bd513
commit dd42d46753
249 changed files with 67417 additions and 6659 deletions
+168
View File
@@ -0,0 +1,168 @@
"""Each status the app documents raises its own exception."""
from __future__ import annotations
import json
import pytest
from fake_donut import FakeDonut, QueuedResponse
from donutbrowser import (
BadGateway,
Conflict,
DonutAPIError,
DonutClient,
DonutConnectionError,
DonutError,
Forbidden,
NotFound,
PaymentRequired,
RateLimited,
RequestTimeout,
ServerError,
ServiceUnavailable,
Unauthorized,
ValidationError,
)
STATUS_TO_ERROR = [
(400, ValidationError),
(401, Unauthorized),
(402, PaymentRequired),
(403, Forbidden),
(404, NotFound),
(408, RequestTimeout),
(409, Conflict),
(429, RateLimited),
(500, ServerError),
(502, BadGateway),
(503, ServiceUnavailable),
]
@pytest.mark.parametrize("status,expected", STATUS_TO_ERROR)
def test_status_maps_to_its_exception(
client: DonutClient, fake: FakeDonut, status: int, expected: type
) -> None:
fake.enqueue_error(status, "something went wrong")
with pytest.raises(expected) as raised:
client.list_profiles()
assert raised.value.status == status
assert raised.value.body == "something went wrong"
assert raised.value.method == "GET"
assert raised.value.path == "/v1/profiles"
def test_every_error_is_a_donut_error(client: DonutClient, fake: FakeDonut) -> None:
fake.enqueue_error(404, "PROFILE_NOT_FOUND")
with pytest.raises(DonutError):
client.get_profile("nope")
def test_the_five_hundreds_share_one_base(client: DonutClient, fake: FakeDonut) -> None:
"""`except ServerError` has to catch 502 and 503 as well as 500."""
for status in (500, 502, 503):
fake.enqueue_error(status, "upstream")
with pytest.raises(ServerError):
client.list_profiles()
def test_rate_limited_carries_retry_after(client: DonutClient, fake: FakeDonut) -> None:
fake.enqueue_error(
429,
"automation request rate limit exceeded",
headers=(("Retry-After", "42"),),
)
with pytest.raises(RateLimited) as raised:
client.run_profile("p1")
assert raised.value.retry_after == 42
def test_rate_limited_without_the_header_is_still_raised(
client: DonutClient, fake: FakeDonut
) -> None:
fake.enqueue_error(429, "slow down")
with pytest.raises(RateLimited) as raised:
client.run_profile("p1")
assert raised.value.retry_after is None
def test_an_unreadable_retry_after_does_not_break_the_error(
client: DonutClient, fake: FakeDonut
) -> None:
fake.enqueue_error(429, "slow down", headers=(("Retry-After", "Wed, 21 Oct 2026 07:28:00 GMT"),))
with pytest.raises(RateLimited) as raised:
client.run_profile("p1")
assert raised.value.retry_after is None
def test_a_structured_code_body_is_decoded(client: DonutClient, fake: FakeDonut) -> None:
"""The app shares `{"code": ...}` strings with its own frontend."""
fake.enqueue_error(400, json.dumps({"code": "NAME_CANNOT_BE_EMPTY"}))
with pytest.raises(ValidationError) as raised:
client.create_group(name="")
assert raised.value.code == "NAME_CANNOT_BE_EMPTY"
assert raised.value.params == {}
def test_a_structured_code_body_keeps_its_params(client: DonutClient, fake: FakeDonut) -> None:
fake.enqueue_error(409, json.dumps({"code": "PROFILE_LOCKED_BY_MEMBER", "params": {"n": "5"}}))
with pytest.raises(Conflict) as raised:
client.run_profile("p1")
assert raised.value.code == "PROFILE_LOCKED_BY_MEMBER"
assert raised.value.params == {"n": "5"}
def test_a_plain_text_body_leaves_code_unset(client: DonutClient, fake: FakeDonut) -> None:
fake.enqueue_error(400, "invalid browser")
with pytest.raises(ValidationError) as raised:
client.create_profile(name="x", browser="chromium")
assert raised.value.code is None
assert raised.value.body == "invalid browser"
def test_an_undocumented_status_still_raises_something_catchable(
client: DonutClient, fake: FakeDonut
) -> None:
fake.enqueue_error(418, "teapot")
with pytest.raises(DonutAPIError) as raised:
client.list_profiles()
assert raised.value.status == 418
def test_an_undocumented_server_status_is_a_server_error(
client: DonutClient, fake: FakeDonut
) -> None:
fake.enqueue_error(504, "gateway timeout")
with pytest.raises(ServerError):
client.list_profiles()
def test_the_message_names_the_call(client: DonutClient, fake: FakeDonut) -> None:
fake.enqueue_error(404, "Profile not found")
with pytest.raises(NotFound) as raised:
client.get_profile("missing")
assert "404" in str(raised.value)
assert "GET /v1/profiles/missing" in str(raised.value)
def test_an_unreachable_app_is_not_an_api_error(fake: FakeDonut) -> None:
port = fake.port
fake.stop()
with DonutClient(token="t", port=port, timeout=2.0, env={}) as client:
with pytest.raises(DonutConnectionError) as raised:
client.list_profiles()
assert "Local API" in str(raised.value)
def test_a_missing_token_fails_before_any_request() -> None:
with pytest.raises(DonutError) as raised:
DonutClient(env={})
assert "DONUT_API_TOKEN" in str(raised.value)
def test_a_non_json_answer_is_reported_as_such(client: DonutClient, fake: FakeDonut) -> None:
fake.responses.append(QueuedResponse(status=200, body="<html>nope</html>"))
with pytest.raises(DonutError) as raised:
client.list_profiles()
assert "not JSON" in str(raised.value)