import json from dataclasses import dataclass from typing import Any from urllib.request import HTTPErrorProcessor, Request, build_opener, urlopen class _KeepErrorResponses(HTTPErrorProcessor): # urllib's default processor converts every non-2xx into a raised # HTTPError. The RPC endpoint carries its error envelope in the body of a # 4xx/5xx, so the response object has to reach the caller intact. def http_response(self, request, response): return response https_response = http_response _opener = build_opener(_KeepErrorResponses) @dataclass(frozen=True) class Reply: status: int raw: bytes @property def body(self) -> dict[str, Any]: # Django's own CSRF and 500 pages are HTML, so decoding is deferred to # the tests that actually assert on the mizan envelope. return json.loads(self.raw) def _send(req: Request) -> Reply: resp = _opener.open(req) return Reply(status=resp.status, raw=resp.read()) class LiveRPCMixin: """HTTP access to the mizan endpoints on a LiveServerTestCase server.""" csrf_token: str = "" cookies: str = "" def session_init(self) -> None: url = f"{self.live_server_url}/api/mizan/session/" resp = urlopen(Request(url)) for cookie in resp.headers.get_all("Set-Cookie") or []: if "csrftoken=" in cookie: self.csrf_token = cookie.split("csrftoken=")[1].split(";")[0] self.cookies = f"csrftoken={self.csrf_token}" return self.csrf_token = "" self.cookies = "" def get(self, path: str) -> Reply: return _send(Request(f"{self.live_server_url}{path}")) def post( self, path: str, body: bytes | str, content_type: str = "application/json", with_csrf: bool = True, ) -> Reply: if isinstance(body, str): body = body.encode() req = Request(f"{self.live_server_url}{path}", data=body, method="POST") req.add_header("Content-Type", content_type) if with_csrf and self.csrf_token: req.add_header("X-CSRFToken", self.csrf_token) req.add_header("Cookie", self.cookies) return _send(req) def call(self, fn: str, args: dict | None = None, with_csrf: bool = True) -> Reply: payload = json.dumps({"fn": fn, "args": args or {}}) return self.post("/api/mizan/call/", payload, with_csrf=with_csrf) def result(self, fn: str, args: dict | None = None) -> Any: """The `result` payload of a call that must have succeeded.""" reply = self.call(fn, args) self.assertEqual(reply.status, 200, reply.raw) return reply.body["result"]