update mangacollec

This commit is contained in:
shooter-dev committed 2026-10-07 16:50:30 +02:00
1 parent e4865224cd
commit 4f564e2962
64 files changed
+531 -174

No files matched your search

+201
View File
@@ -0,0 +1,201 @@
"""Tests for HTTP retry behavior through simulated MangaCollec transports."""
import asyncio
from unittest.mock import AsyncMock, call
import httpx
import pytest
from mangacollec.mangacollec import Mangacollec
class TestRequest:
"""Exercise HTTP retries, transport errors and retry eligibility."""
@pytest.mark.parametrize("status_code", [429, 500, 503, 599])
@pytest.mark.parametrize("retry_after, expected_delay", [
("120", 126.0), ("0", 0.0), (None, 0.5), ("invalid", 0.5),
])
def test_retry_then_success(
self,
client: Mangacollec,
responses: list[httpx.Response | httpx.RequestError],
sent_requests: list[httpx.Request],
sleep_mock: AsyncMock,
status_code: int,
retry_after: str | None,
expected_delay: float,
) -> None:
"""Retry rate limits and server errors using the current response's delay."""
headers = {} if retry_after is None else {"retry-after": retry_after}
responses.extend([
httpx.Response(status_code, headers=headers),
httpx.Response(200, json={"series": []}),
])
payload = asyncio.run(client.get("series"))
assert payload == {"series": []}
assert len(sent_requests) == 2
sleep_mock.assert_awaited_once_with(expected_delay)
@pytest.mark.parametrize("status_code", [429, 503])
@pytest.mark.parametrize("retry_after", [None, "120"])
def test_retry_limit(
self,
client: Mangacollec,
responses: list[httpx.Response | httpx.RequestError],
sent_requests: list[httpx.Request],
sleep_mock: AsyncMock,
status_code: int,
retry_after: str | None,
) -> None:
"""Raise the final HTTP error after five waits and six attempts."""
headers = {} if retry_after is None else {"Retry-After": retry_after}
responses.extend(httpx.Response(status_code, headers=headers) for _ in range(6))
final_response = responses[-1]
with pytest.raises(httpx.HTTPStatusError) as error:
asyncio.run(client.get("series"))
assert error.value.response is final_response
assert len(sent_requests) == 6
expected_delays = [0.5, 1.0, 2.0, 4.0, 8.0] if retry_after is None else [126.0] * 5
assert sleep_mock.await_args_list == [call(delay) for delay in expected_delays]
@pytest.mark.parametrize("error_type", [
httpx.ReadTimeout, httpx.ConnectError, httpx.RemoteProtocolError,
])
def test_network_error_resets_header(
self,
client: Mangacollec,
responses: list[httpx.Response | httpx.RequestError],
sent_requests: list[httpx.Request],
sleep_mock: AsyncMock,
error_type: type[httpx.RequestError],
) -> None:
"""Use exponential fallback after a transport error rather than stale headers."""
responses.extend([
httpx.Response(429, headers={"Retry-After": "120"}),
error_type("Simulated network error"),
httpx.Response(200, json={}),
])
assert asyncio.run(client.get("series")) == {}
assert len(sent_requests) == 3
assert sleep_mock.await_args_list == [call(126.0), call(1.0)]
def test_network_error_limit(
self,
client: Mangacollec,
responses: list[httpx.Response | httpx.RequestError],
sent_requests: list[httpx.Request],
sleep_mock: AsyncMock,
) -> None:
"""Propagate the final transport error without an additional wait."""
responses.extend(httpx.ConnectError("Simulated network error") for _ in range(6))
final_error = responses[-1]
with pytest.raises(httpx.ConnectError) as error:
asyncio.run(client.get("series"))
assert error.value is final_error
assert len(sent_requests) == 6
assert sleep_mock.await_args_list == [call(delay) for delay in [0.5, 1.0, 2.0, 4.0, 8.0]]
@pytest.mark.parametrize("method", ["POST", "PUT", "DELETE"])
@pytest.mark.parametrize("status_code", [429, 503])
def test_mutation_not_retried(
self,
client: Mangacollec,
responses: list[httpx.Response | httpx.RequestError],
sent_requests: list[httpx.Request],
sleep_mock: AsyncMock,
method: str,
status_code: int,
) -> None:
"""Preserve immediate errors for resource mutations despite Retry-After."""
responses.append(httpx.Response(status_code, headers={"Retry-After": "120"}))
with pytest.raises(httpx.HTTPStatusError):
asyncio.run(client._request(method, "possessions", data={}))
assert len(sent_requests) == 1
sleep_mock.assert_not_awaited()
@pytest.mark.parametrize("status_code", [400, 401, 403, 404])
def test_other_client_errors_not_retried(
self,
client: Mangacollec,
responses: list[httpx.Response | httpx.RequestError],
sent_requests: list[httpx.Request],
sleep_mock: AsyncMock,
status_code: int,
) -> None:
"""Do not expand retries to other client errors carrying the header."""
responses.append(httpx.Response(status_code, headers={"Retry-After": "120"}))
with pytest.raises(httpx.HTTPStatusError):
asyncio.run(client.get("series"))
assert len(sent_requests) == 1
sleep_mock.assert_not_awaited()
@pytest.mark.parametrize("status_code", [429, 503])
def test_oauth_retry(
self,
client: Mangacollec,
responses: list[httpx.Response | httpx.RequestError],
sent_requests: list[httpx.Request],
sleep_mock: AsyncMock,
status_code: int,
) -> None:
"""Apply the server delay to retriable OAuth POST requests."""
responses.extend([
httpx.Response(status_code, headers={"Retry-After": "120"}),
httpx.Response(200, json={
"access_token": "new-test-token",
"token_type": "Bearer",
"expires_in": 86400,
"created_at": 0,
}),
])
asyncio.run(client.login())
assert client.authorization_header == "Bearer new-test-token"
assert len(sent_requests) == 2
assert all(request.method == "POST" for request in sent_requests)
assert all(request.url.path == "/oauth/token" for request in sent_requests)
sleep_mock.assert_awaited_once_with(126.0)
@pytest.mark.parametrize("status_code", [200, 204])
def test_success_does_not_wait(
self,
client: Mangacollec,
responses: list[httpx.Response | httpx.RequestError],
sleep_mock: AsyncMock,
status_code: int,
) -> None:
"""Return successful responses immediately even if they have Retry-After."""
responses.append(httpx.Response(status_code, headers={"Retry-After": "120"}, json={}))
assert asyncio.run(client.get("series")) == {}
sleep_mock.assert_not_awaited()
def test_logs_scheduled_wait(
self,
client: Mangacollec,
responses: list[httpx.Response | httpx.RequestError],
sleep_mock: AsyncMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Log the retry number and the actual wait duration."""
responses.extend([
httpx.Response(429, headers={"Retry-After": "120"}),
httpx.Response(200, json={}),
])
asyncio.run(client.get("series"))
assert any("reprise 1/5 dans 126.0 secondes" in message for message in caplog.messages)
sleep_mock.assert_awaited_once_with(126.0)