152 lines
5.4 KiB
Python
152 lines
5.4 KiB
Python
from __future__ import annotations
|
|
|
|
import http.client
|
|
import json
|
|
import threading
|
|
import unittest
|
|
from contextlib import contextmanager
|
|
from http.server import ThreadingHTTPServer
|
|
from typing import Any, Iterator, Mapping
|
|
|
|
from arr_web.contracts import MAX_UPLOAD_BYTES, Response, success
|
|
from arr_web.server import handler_for
|
|
|
|
|
|
class RecordingApplication:
|
|
def __init__(self) -> None:
|
|
self.calls: list[dict[str, Any]] = []
|
|
|
|
def handle(
|
|
self,
|
|
method: str,
|
|
path: str,
|
|
headers: Mapping[str, str],
|
|
body: bytes,
|
|
*,
|
|
client_id: str,
|
|
) -> Response:
|
|
self.calls.append(
|
|
{
|
|
"method": method,
|
|
"path": path,
|
|
"headers": dict(headers),
|
|
"body": body,
|
|
"client_id": client_id,
|
|
}
|
|
)
|
|
return Response.json(200, success({"method": method}))
|
|
|
|
|
|
@contextmanager
|
|
def running_server(
|
|
application: RecordingApplication,
|
|
) -> Iterator[tuple[str, int]]:
|
|
server = ThreadingHTTPServer(("127.0.0.1", 0), handler_for(application))
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
host, port = server.server_address
|
|
yield str(host), int(port)
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join(timeout=5)
|
|
|
|
|
|
class PortalHttpAdapterTests(unittest.TestCase):
|
|
def test_patch_and_delete_forward_json_bodies_to_application(self) -> None:
|
|
application = RecordingApplication()
|
|
with running_server(application) as (host, port):
|
|
connection = http.client.HTTPConnection(host, port, timeout=5)
|
|
try:
|
|
patch_body = json.dumps(
|
|
{"draft_id": "bookingdraft-test", "room_type": "U-TWN", "quantity": 2}
|
|
).encode("utf-8")
|
|
connection.request(
|
|
"PATCH",
|
|
"/api/company-reports/source/draft/items/7",
|
|
body=patch_body,
|
|
headers={"Content-Type": "application/json"},
|
|
)
|
|
patch_response = connection.getresponse()
|
|
self.assertEqual(patch_response.status, 200)
|
|
self.assertEqual(patch_response.getheader("X-Content-Type-Options"), "nosniff")
|
|
patch_response.read()
|
|
|
|
delete_body = b'{"draft_id":"bookingdraft-test"}'
|
|
connection.request(
|
|
"DELETE",
|
|
"/api/company-reports/source/draft/items/7",
|
|
body=delete_body,
|
|
headers={"Content-Type": "application/json"},
|
|
)
|
|
delete_response = connection.getresponse()
|
|
self.assertEqual(delete_response.status, 200)
|
|
delete_response.read()
|
|
|
|
batch_delete_body = b'{"draft_id":"bookingdraft-test","item_ids":[7,8]}'
|
|
connection.request(
|
|
"DELETE",
|
|
"/api/company-reports/source/draft/items",
|
|
body=batch_delete_body,
|
|
headers={"Content-Type": "application/json"},
|
|
)
|
|
batch_delete_response = connection.getresponse()
|
|
self.assertEqual(batch_delete_response.status, 200)
|
|
batch_delete_response.read()
|
|
finally:
|
|
connection.close()
|
|
|
|
self.assertEqual(
|
|
[(call["method"], call["body"]) for call in application.calls],
|
|
[
|
|
("PATCH", patch_body),
|
|
("DELETE", delete_body),
|
|
("DELETE", batch_delete_body),
|
|
],
|
|
)
|
|
self.assertEqual(
|
|
[call["path"] for call in application.calls],
|
|
[
|
|
"/api/company-reports/source/draft/items/7",
|
|
"/api/company-reports/source/draft/items/7",
|
|
"/api/company-reports/source/draft/items",
|
|
],
|
|
)
|
|
|
|
def test_mutation_methods_reject_missing_or_oversized_content_length(self) -> None:
|
|
application = RecordingApplication()
|
|
with running_server(application) as (host, port):
|
|
missing = http.client.HTTPConnection(host, port, timeout=5)
|
|
try:
|
|
missing.putrequest("PATCH", "/api/company-reports/source/draft/items/7")
|
|
missing.endheaders()
|
|
missing_response = missing.getresponse()
|
|
self.assertEqual(missing_response.status, 411)
|
|
self.assertEqual(
|
|
json.loads(missing_response.read())["error"]["code"],
|
|
"CONTENT_LENGTH_REQUIRED",
|
|
)
|
|
finally:
|
|
missing.close()
|
|
|
|
oversized = http.client.HTTPConnection(host, port, timeout=5)
|
|
try:
|
|
oversized.putrequest("DELETE", "/api/company-reports/source/draft")
|
|
oversized.putheader("Content-Length", str(MAX_UPLOAD_BYTES + 1))
|
|
oversized.endheaders()
|
|
oversized_response = oversized.getresponse()
|
|
self.assertEqual(oversized_response.status, 413)
|
|
self.assertEqual(
|
|
json.loads(oversized_response.read())["error"]["code"],
|
|
"REQUEST_TOO_LARGE",
|
|
)
|
|
finally:
|
|
oversized.close()
|
|
|
|
self.assertEqual(application.calls, [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|