mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-11 14:07:33 +01:00
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
141 lines
5.0 KiB
Python
141 lines
5.0 KiB
Python
"""Tests for _web_io — body parsing and reply writing."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from http import HTTPStatus
|
|
import io
|
|
import json
|
|
from typing import TYPE_CHECKING, Any, cast
|
|
|
|
import pytest
|
|
|
|
from steam_backlog_enforcer import _web_io
|
|
from steam_backlog_enforcer._web_errors import ApiError
|
|
|
|
if TYPE_CHECKING:
|
|
from http.server import BaseHTTPRequestHandler
|
|
|
|
|
|
class _Handler:
|
|
"""Just enough of ``BaseHTTPRequestHandler`` for the plumbing."""
|
|
|
|
def __init__(self, headers: dict[str, str], body: bytes = b"") -> None:
|
|
self.headers = headers
|
|
self.rfile = io.BytesIO(body)
|
|
self.wfile = io.BytesIO()
|
|
self.sent: list[tuple[str, str]] = []
|
|
self.status: int | None = None
|
|
|
|
def send_response(self, status: int) -> None:
|
|
self.status = status
|
|
|
|
def send_header(self, name: str, value: str) -> None:
|
|
self.sent.append((name, value))
|
|
|
|
def end_headers(self) -> None:
|
|
self.sent.append(("--", "end"))
|
|
|
|
def as_handler(self) -> BaseHTTPRequestHandler:
|
|
return cast("BaseHTTPRequestHandler", self)
|
|
|
|
|
|
def _read(headers: dict[str, str], body: bytes = b"") -> dict[str, Any]:
|
|
return _web_io.read_json_body(_Handler(headers, body).as_handler())
|
|
|
|
|
|
def _body_of(body: bytes, **headers: str) -> dict[str, Any]:
|
|
fields = {
|
|
"Content-Length": str(len(body)),
|
|
"Content-Type": "application/json",
|
|
**headers,
|
|
}
|
|
return _read(fields, body)
|
|
|
|
|
|
class TestReplies:
|
|
def test_ok_and_no_content(self) -> None:
|
|
assert _web_io.ok({"a": 1}).status == HTTPStatus.OK
|
|
assert _web_io.no_content().status == HTTPStatus.NO_CONTENT
|
|
|
|
|
|
class TestReadJsonBody:
|
|
def test_absent_body_is_empty(self) -> None:
|
|
assert _read({}) == {}
|
|
|
|
def test_parses_an_object(self) -> None:
|
|
got = _body_of(b'{"a": 1}', **{"Content-Type": "Application/JSON; x=y"})
|
|
assert got == {"a": 1}
|
|
|
|
def test_chunked_needs_a_length(self) -> None:
|
|
with pytest.raises(ApiError) as err:
|
|
_body_of(b"", **{"Transfer-Encoding": "chunked"})
|
|
assert err.value.status == HTTPStatus.LENGTH_REQUIRED
|
|
|
|
def test_non_numeric_length(self) -> None:
|
|
with pytest.raises(ApiError) as err:
|
|
_read({"Content-Length": "x"})
|
|
assert err.value.status == HTTPStatus.BAD_REQUEST
|
|
|
|
def test_oversized_body(self) -> None:
|
|
with pytest.raises(ApiError) as err:
|
|
_read({"Content-Length": str(_web_io.MAX_BODY_BYTES + 1)})
|
|
assert err.value.status == HTTPStatus.REQUEST_ENTITY_TOO_LARGE
|
|
|
|
def test_wrong_content_type(self) -> None:
|
|
with pytest.raises(ApiError) as err:
|
|
_body_of(b"{}", **{"Content-Type": "text/plain"})
|
|
assert err.value.status == HTTPStatus.UNSUPPORTED_MEDIA_TYPE
|
|
|
|
def test_missing_content_type(self) -> None:
|
|
with pytest.raises(ApiError) as err:
|
|
_read({"Content-Length": "2"}, b"{}")
|
|
assert err.value.status == HTTPStatus.UNSUPPORTED_MEDIA_TYPE
|
|
|
|
@pytest.mark.parametrize("raw", [b"{nope", b"[1]"])
|
|
def test_malformed_or_non_object(self, raw: bytes) -> None:
|
|
with pytest.raises(ApiError) as err:
|
|
_body_of(raw)
|
|
assert err.value.code == "invalid_params"
|
|
|
|
|
|
class TestSending:
|
|
def test_send_bytes_with_and_without_a_body(self) -> None:
|
|
handler = _Handler({})
|
|
_web_io.send_bytes(
|
|
handler.as_handler(), HTTPStatus.OK, b"hi", "text/plain", {"X-A": "1"}
|
|
)
|
|
assert handler.wfile.getvalue() == b"hi"
|
|
assert ("Content-Length", "2") in handler.sent
|
|
assert ("X-A", "1") in handler.sent
|
|
empty = _Handler({})
|
|
_web_io.send_bytes(empty.as_handler(), HTTPStatus.OK, b"", "text/plain")
|
|
assert empty.wfile.getvalue() == b""
|
|
|
|
def test_raw_reply(self) -> None:
|
|
handler = _Handler({})
|
|
reply = _web_io.Reply(HTTPStatus.OK, raw=b"\xff", ctype="image/jpeg")
|
|
_web_io.send_reply(handler.as_handler(), reply)
|
|
assert handler.wfile.getvalue() == b"\xff"
|
|
assert ("Content-Type", "image/jpeg") in handler.sent
|
|
|
|
def test_payloadless_reply_has_no_content_type(self) -> None:
|
|
handler = _Handler({})
|
|
_web_io.send_reply(handler.as_handler(), _web_io.no_content())
|
|
assert handler.status == HTTPStatus.NO_CONTENT
|
|
assert ("Content-Length", "0") in handler.sent
|
|
assert "Content-Type" not in [name for name, _ in handler.sent]
|
|
|
|
def test_json_reply(self) -> None:
|
|
handler = _Handler({})
|
|
_web_io.send_reply(handler.as_handler(), _web_io.ok({"a": 1}))
|
|
assert json.loads(handler.wfile.getvalue()) == {"a": 1}
|
|
assert ("Cache-Control", "no-store") in handler.sent
|
|
|
|
def test_error_reply(self) -> None:
|
|
handler = _Handler({})
|
|
error = ApiError("m", code="rate_limited", retry_after=3)
|
|
_web_io.send_error(handler.as_handler(), error)
|
|
assert handler.status == HTTPStatus.TOO_MANY_REQUESTS
|
|
assert ("Retry-After", "3") in handler.sent
|
|
assert json.loads(handler.wfile.getvalue())["error"] == "rate_limited"
|