From 22f058e6b9d41b6a30b8ffb1aaaed1979d94c2da Mon Sep 17 00:00:00 2001 From: Taddes Date: Wed, 20 Mar 2024 16:09:29 -0400 Subject: [PATCH] refactor: add initial async features for client (#653) update requests to httpx for async support, add websockets support and create async test client. --- tests/integration/async_push_test_client.py | 329 ++++++++++++++++++ tests/integration/push_test_client.py | 72 ++-- .../integration/test_integration_all_rust.py | 114 +++--- tests/poetry.lock | 188 +++++++++- tests/pyproject.toml | 3 + 5 files changed, 618 insertions(+), 88 deletions(-) create mode 100644 tests/integration/async_push_test_client.py diff --git a/tests/integration/async_push_test_client.py b/tests/integration/async_push_test_client.py new file mode 100644 index 000000000..7ad5fa099 --- /dev/null +++ b/tests/integration/async_push_test_client.py @@ -0,0 +1,329 @@ +"""Module containing Test Client for autopush-rs integration tests.""" +import asyncio +import json +import logging +import random +import uuid +from enum import Enum +from typing import Any +from urllib.parse import urlparse + +import httpx +import websockets +from websockets.exceptions import WebSocketException + +logging.basicConfig(level=logging.DEBUG) +log = logging.getLogger(__name__) + + +class ClientMessageType(Enum): + """All variants for Message Type sent from client.""" + + HELLO = "hello" + REGISTER = "register" + UNREGISTER = "unregister" + BROADCAST = "broadcast" + BROADCAST_SUBSCRIBE = "broadcast_subscribe" + ACK = "ack" + NACK = "nack" + PING = "ping" + + +class AsyncPushTestClient: + """Push Test Client for integration tests.""" + + def __init__(self, url) -> None: + self.url: str = url + self.uaid: uuid.UUID | None = None + self.ws: websockets.WebSocketClientProtocol | None = None + self.use_webpush: bool = True + self.channels: dict[str, str] = {} + self.messages: dict[str, list[str]] = {} + self.notif_response: httpx.Response | None = None + self._crypto_key: str = """\ +keyid="http://example.org/bob/keys/123";salt="XZwpw6o37R-6qoZjw6KwAw=="\ +""" + self.headers: dict[str, str] = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:61.0) " + "Gecko/20100101 Firefox/61.0" + } + + async def connect(self, connection_port: int | None = None) -> None: + """Establish a websocket connection to localhost at the provided `connection_port`. + + Parameters + ---------- + connection_port : int, optional + A defined connection port, which is auto-assigned unless specified. + """ + url: str = self.url + if connection_port: # pragma: no cover + url = f"ws://localhost:{connection_port}/" + self.ws = await websockets.connect(uri=url, extra_headers=self.headers) + + async def ws_server_send(self, message: dict) -> None: + """Send message to websocket server. + Serialize dictionary into a JSON object and send to server. + + Parameters + ---------- + message : dict + message content being sent to websocket server. + """ + if not self.ws: + raise WebSocketException("WebSocket client not available as expected.") + payload: str = json.dumps(message) + log.debug(f"Send: {payload}") + await self.ws.send(payload) + + async def hello(self, uaid: str | None = None, services: list[str] | None = None): + """Hello verification.""" + if not self.ws: + raise WebSocketException("WebSocket client not available as expected.") + + if self.channels: + channels = list(self.channels.keys()) + else: + channels = [] + hello_content: dict[str, Any] = dict( + messageType=ClientMessageType.HELLO.value, use_webpush=True, channelIDs=channels + ) + + if uaid or self.uaid: + hello_content["uaid"] = uaid or self.uaid + + if services: # pragma: no cover + hello_content["broadcasts"] = services + + await self.ws_server_send(message=hello_content) + + reply = await self.ws.recv() + log.debug(f"Recv: {reply!r} ({len(reply)})") + result = json.loads(reply) + + assert result["status"] == 200 + if self.uaid and self.uaid != result["uaid"]: # pragma: no cover + log.debug(f"Mismatch on re-using uaid. Old: {self.uaid}, New: {result['uaid']}") + self.channels = {} + self.uaid = result["uaid"] + return result + + async def broadcast_subscribe(self, services: list[str]) -> None: + """Broadcast WebSocket subscribe.""" + if not self.ws: + raise WebSocketException("WebSocket client not available as expected.") + + message: dict = dict( + messageType=ClientMessageType.BROADCAST_SUBSCRIBE.value, broadcasts=services + ) + await self.ws_server_send(message=message) + + async def register(self, channel_id: str | None = None, key=None, status=200): + """Register a new endpoint for the provided ChannelID. + Optionally locked to the provided VAPID Public key. + """ + if not self.ws: + raise WebSocketException("WebSocket client not available as expected.") + + chid: str = channel_id or str(uuid.uuid4()) + message: dict = dict(messageType=ClientMessageType.REGISTER.value, channelID=chid, key=key) + await self.ws_server_send(message=message) + rcv = await self.ws.recv() + result: Any = json.loads(rcv) + log.debug(f"Recv: {result}") + assert result["status"] == status + assert result["channelID"] == chid + if status == 200: + self.channels[chid] = result["pushEndpoint"] + return result + + async def unregister(self, chid) -> Any: + """Unregister the ChannelID, which should invalidate the associated Endpoint.""" + if not self.ws: + raise WebSocketException("WebSocket client not available as expected") + + message: dict = dict(messageType=ClientMessageType.UNREGISTER.value, channelID=chid) + await self.ws_server_send(message=message) + + rcv = await self.ws.recv() + result = json.loads(rcv) + log.debug(f"Recv: {result}") + return result + + async def delete_notification(self, channel, message=None, status=204) -> httpx.Response: + """Sender (non-client) notification delete. + From perspective of sender, not the client. Implements HTTP client to interact with + notification. + """ + messages = self.messages[channel] + if not message: + message = random.choice(messages) + + log.debug(f"Delete: {message}") + url = urlparse(message) + async with httpx.AsyncClient() as httpx_client: + resp = await httpx_client.delete(url=url.geturl(), timeout=30) + return resp + + async def send_notification( + self, + channel=None, + version=None, + data: str | None = None, + use_header: bool = True, + status: int = 201, + # 202 status reserved for yet to be implemented push w/ reciept. + ttl: int = 200, + timeout: float | int = 0.2, + vapid: dict = {}, + endpoint: str | None = None, + topic: str | None = None, + headers: dict = {}, + ): + """Sender (not-client) sent notification. + Not part of responsibility of client but a subscribed sender. + Calling from PushTestClient provides introspection of values in + both client interface and the sender. + """ + if not channel: + channel = random.choice(list(self.channels.keys())) + + endpoint = endpoint or self.channels[channel] + url = urlparse(endpoint) + + headers = {} + if ttl is not None: + headers.update({"TTL": str(ttl)}) + if use_header: + headers.update( + { + "Content-Type": "application/octet-stream", + "Content-Encoding": "aesgcm", + "Encryption": self._crypto_key, + "Crypto-Key": 'keyid="a1"; dh="JcqK-OLkJZlJ3sJJWstJCA"', + } + ) + if vapid: + headers.update({"Authorization": f"Bearer {vapid.get('auth')}".rstrip()}) + ckey: str = f'p256ecdsa="{vapid.get("crypto-key")}"' + headers.update({"Crypto-Key": f"{headers.get('Crypto-Key', '')};{ckey}"}) + if topic: + headers["Topic"] = topic + body: str = data or "" + method: str = "POST" + log.debug(f"{method} body: {body}") + log.debug(f" headers: {headers}") + async with httpx.AsyncClient() as httpx_client: + resp = await httpx_client.request( + method=method, url=url.geturl(), content=body, headers=headers + ) + log.debug(f"{method} Response ({resp.status_code}): {resp.text}") + assert resp.status_code == status, f"Expected {status}, got {resp.status_code}" + self.notif_response = resp + location = resp.headers.get("Location", None) + log.debug(f"Response Headers: {resp.headers}") + if status >= 200 and status < 300: + assert location is not None + if status == 201 and ttl is not None: + ttl_header = resp.headers.get("TTL") + assert ttl_header == str(ttl) + if ttl != 0 and status == 201: + assert location is not None + if channel in self.messages: + self.messages[channel].append(location) + else: + self.messages[channel] = [location] + # Pull the sent notification immediately if connected. + # Calls `get_notification` to get response from websocket. + if self.ws and self.ws.is_client: # check back on this after integration + return object.__getattribute__(self, "get_notification")(timeout) + else: + return resp + + async def get_notification(self, timeout=1): + """Get most recent notification from websocket server. + Typically called after a `send_notification` is sent from client to server. + Method to recieve response from websocket. + + Includes ability to define a timeout to simulate latency. + """ + if not self.ws: + raise WebSocketException("WebSocket client not available as expected") + + try: + d = await asyncio.wait_for(self.ws.recv(), timeout) + log.debug(f"Recv: {d!r}") + return json.loads(d) + except Exception: + return None + + async def get_broadcast(self, timeout=1): # pragma: no cover + """Get broadcast.""" + if not self.ws: + raise WebSocketException("WebSocket client not available as expected") + + try: + d = await asyncio.wait_for(self.ws.recv(), timeout) + log.debug(f"Recv: {d}") + result = json.loads(d) + assert result.get("messageType") == ClientMessageType.BROADCAST.value + return result + except WebSocketException as ex: # pragma: no cover + log.error(f"Error: {ex}") + return None + + async def ping(self): + """Test websocket ping.""" + if not self.ws: + raise Exception("WebSocket client not available as expected.") + + log.debug("Send: {}") + await self.ws.ping("{}") + result = await self.ws.recv() + log.debug(f"Recv: {result}") + return result + + async def ack(self, channel, version) -> None: + """Acknowledge message send.""" + if not self.ws: + raise WebSocketException("WebSocket client not available as expected.") + + message: str = json.dumps( + dict( + messageType=ClientMessageType.ACK.value, + updates=[dict(channelID=channel, version=version)], + ) + ) + log.debug(f"Send: {message}") + await self.ws.send(message) + + async def disconnect(self) -> None: + """Disconnect from the application websocket.""" + if not self.ws: + raise WebSocketException("WebSocket client not available as expected.") + + await self.ws.close() + + async def sleep(self, duration: int) -> None: # pragma: no cover + """Sleep wrapper function.""" + await asyncio.sleep(duration) + + async def send_bad_data(self) -> None: + """Send `bad-data` as a string. Used in determining Sentry output + in autoconnect to ensure error logs indicate disconnection. + """ + if not self.ws: + raise WebSocketException("WebSocket client not available as expected.") + + await self.ws.send("bad-data") + + async def wait_for(self, func) -> None: + """Wait several seconds for a function to return True.""" + # This function currently not used for anything so may be removable. + # However, it may have had historic value when dealing with latency. + times = 0 + while not func(): # pragma: no cover + await asyncio.sleep(1) + times += 1 + if times > 9: # pragma: no cover + break diff --git a/tests/integration/push_test_client.py b/tests/integration/push_test_client.py index d3604b151..11f34f0b2 100644 --- a/tests/integration/push_test_client.py +++ b/tests/integration/push_test_client.py @@ -8,7 +8,7 @@ from typing import Any from urllib.parse import urlparse -import requests +import httpx import websocket from twisted.internet.threads import deferToThread @@ -38,8 +38,8 @@ def __init__(self, url) -> None: self.ws: websocket.WebSocket | None = None self.use_webpush: bool = True self.channels: dict[str, str] = {} - self.messages: dict[str, str] = {} - self.notif_response: requests.Response | None = None + self.messages: dict[str, list[str]] = {} + self.notif_response: httpx.Response | None = None self._crypto_key: str = """\ keyid="http://example.org/bob/keys/123";salt="XZwpw6o37R-6qoZjw6KwAw=="\ """ @@ -113,7 +113,6 @@ def hello(self, uaid: str | None = None, services: list[str] | None = None): log.debug(f"Recv: {reply!r} ({len(reply)})") result = json.loads(reply) assert result["status"] == 200 - assert "-" not in result["uaid"] if self.uaid and self.uaid != result["uaid"]: # pragma: no cover log.debug(f"Mismatch on re-using uaid. Old: {self.uaid}, New: {result['uaid']}") self.channels = {} @@ -160,32 +159,40 @@ def unregister(self, chid) -> Any: log.debug(f"Recv: {result}") return result - def delete_notification(self, channel, message=None, status=204) -> requests.Response: - """Delete notification.""" + def delete_notification(self, channel, message=None, status=204) -> httpx.Response: + """Sender (non-client) notification delete. + From perspective of sender, not the client. Implements HTTP client to interact with + notification. + """ messages = self.messages[channel] if not message: message = random.choice(messages) log.debug(f"Delete: {message}") url = urlparse(message) - resp = requests.delete(url=url.geturl(), timeout=30) + resp = httpx.delete(url=url.geturl(), timeout=30) return resp def send_notification( self, channel=None, version=None, - data=None, - use_header=True, - status=None, - ttl=200, - timeout=0.2, - vapid=None, - endpoint=None, - topic=None, - headers=None, + data: str | None = None, + use_header: bool = True, + status: int = 201, + # 202 status reserved for yet to be implemented push w/ reciept. + ttl: int = 200, + timeout: float | int = 0.2, + vapid: dict = {}, + endpoint: str | None = None, + topic: str | None = None, + headers: dict = {}, ): - """Send notification.""" + """Sender (not-client) sent notification. + Not part of responsibility of client but a subscribed sender. + Calling from PushTestClient provides introspection of values in + both client interface and the sender. + """ if not channel: channel = random.choice(list(self.channels.keys())) @@ -194,7 +201,7 @@ def send_notification( headers = {} if ttl is not None: - headers = {"TTL": str(ttl)} + headers.update({"TTL": str(ttl)}) if use_header: headers.update( { @@ -205,19 +212,16 @@ def send_notification( } ) if vapid: - headers.update({"Authorization": "Bearer " + vapid.get("auth")}) - ckey = 'p256ecdsa="' + vapid.get("crypto-key") + '"' - headers.update({"Crypto-Key": headers.get("Crypto-Key", "") + ";" + ckey}) + headers.update({"Authorization": f"Bearer {vapid.get('auth')}".rstrip()}) + ckey: str = f'p256ecdsa="{vapid.get("crypto-key")}"' + headers.update({"Crypto-Key": f"{headers.get('Crypto-Key', '')};{ckey}"}) if topic: headers["Topic"] = topic - body = data or "" - method = "POST" - # 202 status reserved for yet to be implemented push w/ reciept. - status = status or 201 - + body: str = data or "" + method: str = "POST" log.debug(f"{method} body: {body}") log.debug(f" headers: {headers}") - resp = requests.request(method=method, url=url.geturl(), data=body, headers=headers) + resp = httpx.request(method=method, url=url.geturl(), content=body, headers=headers) log.debug(f"{method} Response ({resp.status_code}): {resp.text}") assert resp.status_code == status, f"Expected {status}, got {resp.status_code}" self.notif_response = resp @@ -234,14 +238,20 @@ def send_notification( self.messages[channel].append(location) else: self.messages[channel] = [location] - # Pull the notification if connected + # Pull the sent notification immediately if connected. + # Calls `get_notification` to get response from websocket. if self.ws and self.ws.connected: return object.__getattribute__(self, "get_notification")(timeout) else: return resp def get_notification(self, timeout=1): - """Get notification.""" + """Get most recent notification from websocket server. + Typically called after a `send_notification` is sent from client to server. + Method to recieve response from websocket. + + Includes ability to define a timeout to simulate latency. + """ if not self.ws: raise Exception("WebSocket client not available as expected") @@ -267,8 +277,7 @@ def get_broadcast(self, timeout=1): # pragma: no cover d = self.ws.recv() log.debug(f"Recv: {d}") result = json.loads(d) - # ASK JR - # assert result.get("messageType") == ClientMessageType.BROADCAST.value + assert result.get("messageType") == ClientMessageType.BROADCAST.value return result except Exception as ex: # pragma: no cover log.error(f"Error: {ex}") @@ -319,6 +328,7 @@ def send_bad_data(self) -> None: """ if not self.ws: raise Exception("WebSocket client not available as expected.") + self.ws.send("bad-data") def wait_for(self, func): diff --git a/tests/integration/test_integration_all_rust.py b/tests/integration/test_integration_all_rust.py index 4a187d4c4..f3e2fab14 100644 --- a/tests/integration/test_integration_all_rust.py +++ b/tests/integration/test_integration_all_rust.py @@ -21,8 +21,9 @@ import bottle import ecdsa +import httpx import psutil -import requests +import pytest import twisted.internet.base from cryptography.fernet import Fernet from jose import jws @@ -30,6 +31,7 @@ from twisted.internet.defer import inlineCallbacks, returnValue from twisted.trial import unittest +from .async_push_test_client import AsyncPushTestClient from .db import ( DynamoDBResource, base64url_encode, @@ -642,7 +644,7 @@ def test_sentry_output_autoconnect(self): yield self.shut_down(client) # LogCheck does throw an error every time - requests.get("http://localhost:{}/v1/err/crit".format(CONNECTION_PORT), timeout=30) + httpx.get(f"http://localhost:{CONNECTION_PORT}/v1/err/crit", timeout=30) event1 = MOCK_SENTRY_QUEUE.get(timeout=5) # new autoconnect emits 2 events try: @@ -663,7 +665,7 @@ def test_sentry_output_autoendpoint(self): endpoint = self.host_endpoint(client) yield self.shut_down(client) - requests.get("{}/__error__".format(endpoint), timeout=30) + httpx.get(f"{endpoint}/__error__", timeout=30) # 2 events excpted: 1 from a panic and 1 from a returned Error event1 = MOCK_SENTRY_QUEUE.get(timeout=5) event2 = MOCK_SENTRY_QUEUE.get(timeout=1) @@ -681,8 +683,8 @@ def test_no_sentry_output(self): return ws_url = urlparse(self._ws_url)._replace(scheme="http").geturl() try: - requests.get(ws_url, timeout=30) - except requests.exceptions.ConnectionError: + httpx.get(ws_url, timeout=30) + except httpx.ConnectError: pass try: data = MOCK_SENTRY_QUEUE.get(timeout=1) @@ -1379,19 +1381,19 @@ def test_internal_endpoints(self): url = parsed._replace(netloc=f"{parsed.hostname}:{ROUTER_PORT}").geturl() # First ensure the endpoint we're testing for on the public port exists where # we expect it on the internal ROUTER_PORT - requests.put(url, timeout=30).raise_for_status() + httpx.put(url, timeout=30).raise_for_status() try: - requests.put(parsed.geturl(), timeout=30).raise_for_status() - except requests.exceptions.ConnectionError: + httpx.put(parsed.geturl(), timeout=30).raise_for_status() + except httpx.ConnectError: pass - except requests.exceptions.HTTPError as e: + except httpx.HTTPError as e: assert e.response.status_code == 404 else: assert False -class TestRustWebPushBroadcast(unittest.TestCase): +class TestRustWebPushBroadcast: """Test class for Rust Web Push Broadcast.""" max_endpoint_logs = 4 @@ -1401,28 +1403,28 @@ def tearDown(self): """Tear down.""" process_logs(self) - @inlineCallbacks - def quick_register(self, connection_port=None): + @pytest.mark.asyncio + async def quick_register(self, connection_port=None): """Connect and register client.""" conn_port = connection_port or MP_CONNECTION_PORT - client = PushTestClient("ws://localhost:{}/".format(conn_port)) - yield client.connect() - yield client.hello() - yield client.register() + client = AsyncPushTestClient(f"ws://localhost:{conn_port}/") + await client.connect() + await client.hello() + await client.register() returnValue(client) - @inlineCallbacks - def shut_down(self, client=None): + @pytest.mark.asyncio + async def shut_down(self, client=None): """Shut down client connection.""" if client: - yield client.disconnect() + await client.disconnect() @property def _ws_url(self): - return "ws://localhost:{}/".format(MP_CONNECTION_PORT) + return f"ws://localhost:{MP_CONNECTION_PORT}/" - @inlineCallbacks - def test_broadcast_update_on_connect(self): + @pytest.mark.asyncio + async def test_broadcast_update_on_connect(self): """Test that the client receives any pending broadcast updates on connect.""" global MOCK_MP_SERVICES MOCK_MP_SERVICES = {"kinto:123": "ver1"} @@ -1430,9 +1432,9 @@ def test_broadcast_update_on_connect(self): MOCK_MP_POLLED.wait(timeout=5) old_ver = {"kinto:123": "ver0"} - client = PushTestClient(self._ws_url) - yield client.connect() - result = yield client.hello(services=old_ver) + client = AsyncPushTestClient(self._ws_url) + await client.connect() + result = await client.hello(services=old_ver) assert result != {} assert result["use_webpush"] is True assert result["broadcasts"]["kinto:123"] == "ver1" @@ -1441,14 +1443,14 @@ def test_broadcast_update_on_connect(self): MOCK_MP_POLLED.clear() MOCK_MP_POLLED.wait(timeout=5) - result = yield client.get_broadcast(2) + result = await client.get_broadcast(2) assert result.get("messageType") == ClientMessageType.BROADCAST.value assert result["broadcasts"]["kinto:123"] == "ver2" - yield self.shut_down(client) + await self.shut_down(client) - @inlineCallbacks - def test_broadcast_update_on_connect_with_errors(self): + @pytest.mark.asyncio + async def test_broadcast_update_on_connect_with_errors(self): """Test that the client can receive broadcast updates on connect that may have produced internal errors. """ @@ -1458,17 +1460,17 @@ def test_broadcast_update_on_connect_with_errors(self): MOCK_MP_POLLED.wait(timeout=5) old_ver = {"kinto:123": "ver0", "kinto:456": "ver1"} - client = PushTestClient(self._ws_url) - yield client.connect() - result = yield client.hello(services=old_ver) + client = AsyncPushTestClient(self._ws_url) + await client.connect() + result = await client.hello(services=old_ver) assert result != {} assert result["use_webpush"] is True assert result["broadcasts"]["kinto:123"] == "ver1" assert result["broadcasts"]["errors"]["kinto:456"] == "Broadcast not found" - yield self.shut_down(client) + await self.shut_down(client) - @inlineCallbacks - def test_broadcast_subscribe(self): + @pytest.mark.asyncio + async def test_broadcast_subscribe(self): """Test that the client can subscribe to new broadcasts.""" global MOCK_MP_SERVICES MOCK_MP_SERVICES = {"kinto:123": "ver1"} @@ -1476,15 +1478,15 @@ def test_broadcast_subscribe(self): MOCK_MP_POLLED.wait(timeout=5) old_ver = {"kinto:123": "ver0"} - client = PushTestClient(self._ws_url) - yield client.connect() - result = yield client.hello() + client = AsyncPushTestClient(self._ws_url) + await client.connect() + result = await client.hello() assert result != {} assert result["use_webpush"] is True assert result["broadcasts"] == {} - client.broadcast_subscribe(old_ver) - result = yield client.get_broadcast() + await client.broadcast_subscribe(old_ver) + result = await client.get_broadcast() assert result.get("messageType") == ClientMessageType.BROADCAST.value assert result["broadcasts"]["kinto:123"] == "ver1" @@ -1492,14 +1494,14 @@ def test_broadcast_subscribe(self): MOCK_MP_POLLED.clear() MOCK_MP_POLLED.wait(timeout=5) - result = yield client.get_broadcast(2) + result = await client.get_broadcast(2) assert result.get("messageType") == ClientMessageType.BROADCAST.value assert result["broadcasts"]["kinto:123"] == "ver2" - yield self.shut_down(client) + await self.shut_down(client) - @inlineCallbacks - def test_broadcast_subscribe_with_errors(self): + @pytest.mark.asyncio + async def test_broadcast_subscribe_with_errors(self): """Test that broadcast returns expected errors.""" global MOCK_MP_SERVICES MOCK_MP_SERVICES = {"kinto:123": "ver1"} @@ -1507,23 +1509,23 @@ def test_broadcast_subscribe_with_errors(self): MOCK_MP_POLLED.wait(timeout=5) old_ver = {"kinto:123": "ver0", "kinto:456": "ver1"} - client = PushTestClient(self._ws_url) - yield client.connect() - result = yield client.hello() + client = AsyncPushTestClient(self._ws_url) + await client.connect() + result = await client.hello() assert result != {} assert result["use_webpush"] is True assert result["broadcasts"] == {} - client.broadcast_subscribe(old_ver) - result = yield client.get_broadcast() + await client.broadcast_subscribe(old_ver) + result = await client.get_broadcast() assert result.get("messageType") == ClientMessageType.BROADCAST.value assert result["broadcasts"]["kinto:123"] == "ver1" assert result["broadcasts"]["errors"]["kinto:456"] == "Broadcast not found" - yield self.shut_down(client) + await self.shut_down(client) - @inlineCallbacks - def test_broadcast_no_changes(self): + @pytest.mark.asyncio + async def test_broadcast_no_changes(self): """Test to ensure there are no changes from broadcast.""" global MOCK_MP_SERVICES MOCK_MP_SERVICES = {"kinto:123": "ver1"} @@ -1531,11 +1533,11 @@ def test_broadcast_no_changes(self): MOCK_MP_POLLED.wait(timeout=5) old_ver = {"kinto:123": "ver1"} - client = PushTestClient(self._ws_url) - yield client.connect() - result = yield client.hello(services=old_ver) + client = AsyncPushTestClient(self._ws_url) + await client.connect() + result = await client.hello(services=old_ver) assert result != {} assert result["use_webpush"] is True assert result["broadcasts"] == {} - yield self.shut_down(client) + await self.shut_down(client) diff --git a/tests/poetry.lock b/tests/poetry.lock index 4b5857ca6..423a2e7b7 100644 --- a/tests/poetry.lock +++ b/tests/poetry.lock @@ -11,6 +11,26 @@ files = [ {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, ] +[[package]] +name = "anyio" +version = "4.3.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + [[package]] name = "attrs" version = "23.1.0" @@ -884,6 +904,62 @@ files = [ docs = ["Sphinx"] test = ["objgraph", "psutil"] +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.4" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.4-py3-none-any.whl", hash = "sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73"}, + {file = "httpcore-1.0.4.tar.gz", hash = "sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.25.0)"] + +[[package]] +name = "httpx" +version = "0.27.0" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + [[package]] name = "hyperlink" version = "21.0.0" @@ -1616,6 +1692,24 @@ pluggy = ">=0.12,<2.0" [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "0.23.5.post1" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-asyncio-0.23.5.post1.tar.gz", hash = "sha256:b9a8806bea78c21276bc34321bbf234ba1b2ea5b30d9f0ce0f2dea45e4685813"}, + {file = "pytest_asyncio-0.23.5.post1-py3-none-any.whl", hash = "sha256:30f54d27774e79ac409778889880242b0403d09cabd65b727ce90fe92dd5d80e"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + [[package]] name = "python-dateutil" version = "2.8.2" @@ -1935,6 +2029,17 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + [[package]] name = "snowballstemmer" version = "2.2.0" @@ -2082,6 +2187,87 @@ docs = ["Sphinx (>=6.0)", "sphinx-rtd-theme (>=1.1.0)"] optional = ["python-socks", "wsaccel"] test = ["websockets"] +[[package]] +name = "websockets" +version = "12.0" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"}, + {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"}, + {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"}, + {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"}, + {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"}, + {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"}, + {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"}, + {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"}, + {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"}, + {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"}, + {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"}, + {file = "websockets-12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438"}, + {file = "websockets-12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2"}, + {file = "websockets-12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7"}, + {file = "websockets-12.0-cp38-cp38-win32.whl", hash = "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62"}, + {file = "websockets-12.0-cp38-cp38-win_amd64.whl", hash = "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892"}, + {file = "websockets-12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d"}, + {file = "websockets-12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28"}, + {file = "websockets-12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9"}, + {file = "websockets-12.0-cp39-cp39-win32.whl", hash = "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6"}, + {file = "websockets-12.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8"}, + {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"}, + {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"}, + {file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"}, + {file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"}, + {file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"}, + {file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"}, + {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"}, + {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, +] + [[package]] name = "werkzeug" version = "3.0.1" @@ -2173,4 +2359,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "cfd2992aaaaf0f52ea822b40869ac3efe9652598ecdbec52568e798cef940b2b" +content-hash = "2712e8325859f8cb1c8e08c6fa6ab77e4c999ed2516e4dc0c578efbe5f2b0815" diff --git a/tests/pyproject.toml b/tests/pyproject.toml index f61be476b..cc3ad3a19 100644 --- a/tests/pyproject.toml +++ b/tests/pyproject.toml @@ -51,6 +51,7 @@ license = "Mozilla Public License Version 2.0" [tool.poetry.dependencies] python = "^3.12" websocket-client = "^1.7.0" +websockets = "^12.0" [tool.poetry.group.dev.dependencies] black = "^23.12.0" @@ -75,6 +76,8 @@ python-jose = "^3.3.0" requests = "^2.31.0" twisted = "^23.10.0" types-requests = "^2.31.0.10" +pytest-asyncio = "^0.23.5.post1" +httpx = "^0.27.0" [tool.poetry.group.load.dependencies] locust = "^2.20.0"