mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-24 21:38:09 +02:00
163 lines
3.7 KiB
Python
163 lines
3.7 KiB
Python
|
|
"""Tests for async_retry utilities."""
|
||
|
|
|
||
|
|
import httpx
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from app.connectors.exceptions import (
|
||
|
|
ConnectorAPIError,
|
||
|
|
ConnectorAuthError,
|
||
|
|
ConnectorError,
|
||
|
|
ConnectorRateLimitError,
|
||
|
|
ConnectorTimeoutError,
|
||
|
|
)
|
||
|
|
from app.utils.async_retry import _is_retryable, raise_for_status
|
||
|
|
|
||
|
|
pytestmark = pytest.mark.unit
|
||
|
|
|
||
|
|
|
||
|
|
def make_response(
|
||
|
|
status_code: int,
|
||
|
|
*,
|
||
|
|
headers: dict[str, str] | None = None,
|
||
|
|
json_body=None,
|
||
|
|
text_body: str = "",
|
||
|
|
):
|
||
|
|
kwargs = {
|
||
|
|
"status_code": status_code,
|
||
|
|
"headers": headers,
|
||
|
|
"request": httpx.Request("GET", "https://x"),
|
||
|
|
}
|
||
|
|
|
||
|
|
if json_body is not None:
|
||
|
|
kwargs["json"] = json_body
|
||
|
|
else:
|
||
|
|
kwargs["text"] = text_body
|
||
|
|
|
||
|
|
return httpx.Response(**kwargs)
|
||
|
|
|
||
|
|
|
||
|
|
def test_raise_for_status_does_not_raise_for_success():
|
||
|
|
response = make_response(200)
|
||
|
|
|
||
|
|
raise_for_status(response)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize(
|
||
|
|
("retry_after_header", "expected"),
|
||
|
|
[
|
||
|
|
("5", 5.0),
|
||
|
|
(None, None),
|
||
|
|
("abc", None),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
def test_raise_for_status_429(retry_after_header, expected):
|
||
|
|
headers = {}
|
||
|
|
if retry_after_header is not None:
|
||
|
|
headers["Retry-After"] = retry_after_header
|
||
|
|
|
||
|
|
response = make_response(
|
||
|
|
429,
|
||
|
|
headers=headers,
|
||
|
|
json_body={"detail": "rate limited"},
|
||
|
|
)
|
||
|
|
|
||
|
|
with pytest.raises(ConnectorRateLimitError) as exc_info:
|
||
|
|
raise_for_status(response)
|
||
|
|
|
||
|
|
exc = exc_info.value
|
||
|
|
assert exc.retry_after == expected
|
||
|
|
assert exc.response_body == {"detail": "rate limited"}
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("status_code", [401, 403])
|
||
|
|
def test_raise_for_status_auth_errors(status_code):
|
||
|
|
response = make_response(
|
||
|
|
status_code,
|
||
|
|
json_body={"error": "unauthorized"},
|
||
|
|
)
|
||
|
|
|
||
|
|
with pytest.raises(ConnectorAuthError) as exc_info:
|
||
|
|
raise_for_status(response)
|
||
|
|
|
||
|
|
exc = exc_info.value
|
||
|
|
assert exc.status_code == status_code
|
||
|
|
assert exc.response_body == {"error": "unauthorized"}
|
||
|
|
|
||
|
|
|
||
|
|
def test_raise_for_status_gateway_timeout():
|
||
|
|
response = make_response(
|
||
|
|
504,
|
||
|
|
json_body={"error": "timeout"},
|
||
|
|
)
|
||
|
|
|
||
|
|
with pytest.raises(ConnectorTimeoutError):
|
||
|
|
raise_for_status(response)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("status_code", [500, 502])
|
||
|
|
def test_raise_for_status_server_errors(status_code):
|
||
|
|
response = make_response(
|
||
|
|
status_code,
|
||
|
|
json_body={"error": "server"},
|
||
|
|
)
|
||
|
|
|
||
|
|
with pytest.raises(ConnectorAPIError) as exc_info:
|
||
|
|
raise_for_status(response)
|
||
|
|
|
||
|
|
assert exc_info.value.status_code == status_code
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("status_code", [400, 404])
|
||
|
|
def test_raise_for_status_client_errors(status_code):
|
||
|
|
response = make_response(
|
||
|
|
status_code,
|
||
|
|
json_body={"error": "client"},
|
||
|
|
)
|
||
|
|
|
||
|
|
with pytest.raises(ConnectorAPIError) as exc_info:
|
||
|
|
raise_for_status(response)
|
||
|
|
|
||
|
|
assert exc_info.value.status_code == status_code
|
||
|
|
|
||
|
|
|
||
|
|
def test_raise_for_status_uses_text_when_json_parsing_fails():
|
||
|
|
response = make_response(
|
||
|
|
500,
|
||
|
|
text_body="Internal server error",
|
||
|
|
)
|
||
|
|
|
||
|
|
with pytest.raises(ConnectorAPIError) as exc_info:
|
||
|
|
raise_for_status(response)
|
||
|
|
|
||
|
|
assert exc_info.value.response_body == "Internal server error"
|
||
|
|
|
||
|
|
|
||
|
|
def test_connector_error_retryable_false():
|
||
|
|
exc = ConnectorError("boom")
|
||
|
|
|
||
|
|
assert _is_retryable(exc) is False
|
||
|
|
|
||
|
|
|
||
|
|
def test_rate_limit_error_is_retryable():
|
||
|
|
exc = ConnectorRateLimitError()
|
||
|
|
|
||
|
|
assert _is_retryable(exc) is True
|
||
|
|
|
||
|
|
|
||
|
|
def test_timeout_exception_is_retryable():
|
||
|
|
exc = httpx.TimeoutException("timeout")
|
||
|
|
|
||
|
|
assert _is_retryable(exc) is True
|
||
|
|
|
||
|
|
|
||
|
|
def test_connect_error_is_retryable():
|
||
|
|
exc = httpx.ConnectError("connection failed")
|
||
|
|
|
||
|
|
assert _is_retryable(exc) is True
|
||
|
|
|
||
|
|
|
||
|
|
def test_unrelated_exception_is_not_retryable():
|
||
|
|
exc = ValueError("boom")
|
||
|
|
|
||
|
|
assert _is_retryable(exc) is False
|