ringcentral.websocket.web_socket_client_test

  1import asyncio
  2import contextlib
  3import json
  4import unittest
  5import uuid
  6from unittest import mock
  7
  8from .events import WebSocketEvents
  9from .web_socket_client import WebSocketClient
 10
 11
 12class FakeResponse:
 13    def __init__(self, payload):
 14        self._payload = payload
 15
 16    def json_dict(self):
 17        return self._payload
 18
 19
 20class FakePlatform:
 21    def post(self, path, body=None):
 22        return FakeResponse({
 23            "uri": "wss://fake-websocket",
 24            "ws_access_token": "fake-ws-token",
 25        })
 26
 27
 28class FakeWebSocket:
 29    def __init__(self):
 30        self.sent = []
 31        self.received = []
 32        self.closed = False
 33        self._fail_next_recv = False
 34        self._receive_error = None
 35        self._frames = asyncio.Queue()
 36
 37    def fail_next_receive(self, error):
 38        self._fail_next_recv = True
 39        self._receive_error = error
 40
 41    def push(self, frame):
 42        self._frames.put_nowait(frame)
 43
 44    def has_pending_frames(self):
 45        return self._frames.qsize() > 0
 46
 47    async def recv(self):
 48        if self._fail_next_recv:
 49            self._fail_next_recv = False
 50            raise self._receive_error
 51        frame = await self._frames.get()
 52        if frame is None:
 53            raise ConnectionResetError("connection closed")
 54        self.received.append(frame)
 55        return frame
 56
 57    async def send(self, message):
 58        self.sent.append(message)
 59
 60    async def close(self):
 61        self.closed = True
 62        self._frames.put_nowait(None)
 63
 64
 65class GatedSendWebSocket(FakeWebSocket):
 66    def __init__(self):
 67        super().__init__()
 68        self.send_started = asyncio.Event()
 69        self.send_gate = asyncio.Event()
 70
 71    async def send(self, message):
 72        self.send_started.set()
 73        await self.send_gate.wait()
 74        await FakeWebSocket.send(self, message)
 75
 76
 77class FlakySendWebSocket(FakeWebSocket):
 78    def __init__(self, error):
 79        super().__init__()
 80        self._send_error = error
 81
 82    async def send(self, message):
 83        if self._send_error is not None:
 84            error = self._send_error
 85            self._send_error = None
 86            raise error
 87        await FakeWebSocket.send(self, message)
 88
 89
 90class RecordingHandler:
 91    def __init__(self, error=None):
 92        self.calls = []
 93        self.error = error
 94
 95    def __call__(self, *args):
 96        self.calls.append(args)
 97        if self.error is not None:
 98            raise self.error
 99
100
101EVENT_FILTERS = ["/restapi/v1.0/account/~/extension/~/presence"]
102OTHER_FILTERS = ["/restapi/v1.0/account/~/extension/~/message-store"]
103
104
105def subscription_creation_frame(message_id, status=200):
106    return json.dumps([
107        {
108            "type": "ClientRequest",
109            "messageId": message_id,
110            "status": status,
111        },
112        {
113            "uri": "/restapi/v1.0/subscription/fake-subscription",
114            "id": "fake-subscription",
115            "status": "Active",
116            "eventFilters": EVENT_FILTERS,
117            "deliveryMode": {"transportType": "WebSocket"},
118        },
119    ])
120
121
122def subscription_removal_frame(message_id):
123    return json.dumps([
124        {
125            "type": "ClientRequest",
126            "messageId": message_id,
127            "status": 200,
128        },
129    ])
130
131
132def server_notification_frame():
133    return json.dumps([
134        {
135            "type": "ServerNotification",
136            "messageId": str(uuid.uuid4()),
137        },
138        {
139            "uri": "/restapi/v1.0/subscription/fake-subscription",
140            "event": {"/restapi/v1.0/account/~/extension/~/presence": {"activeCalls": []}},
141        },
142    ])
143
144
145class WebSocketClientTest(unittest.IsolatedAsyncioTestCase):
146    def setUp(self):
147        self.client = WebSocketClient(FakePlatform())
148
149    async def start_receive_loop(self):
150        fake_socket = FakeWebSocket()
151
152        async def connect(uri):
153            return fake_socket
154
155        patcher = mock.patch("websockets.connect", connect)
156        patcher.start()
157        self.addCleanup(patcher.stop)
158        connection_task = asyncio.create_task(self.client.create_new_connection())
159        return fake_socket, connection_task
160
161    async def start_receive_loop_with(self, fake_socket):
162        async def connect(uri):
163            return fake_socket
164
165        patcher = mock.patch("websockets.connect", connect)
166        patcher.start()
167        self.addCleanup(patcher.stop)
168        connection_task = asyncio.create_task(self.client.create_new_connection())
169        fake_socket.push("[/heartbeat]")
170        await self.wait_until(lambda: self.client._is_ready)
171        return connection_task
172
173    async def deliver_creation_response(self, fake_socket, status=200):
174        request = json.loads(fake_socket.sent[0])
175        fake_socket.push(subscription_creation_frame(request[0]["messageId"], status=status))
176
177    async def wait_until(self, predicate, limit=100):
178        for _ in range(limit):
179            if predicate():
180                return
181            await asyncio.sleep(0)
182        self.fail("condition was not met before the spin limit")
183
184    async def wait_until_frames_delivered(self, fake_socket):
185        await self.wait_until(lambda: not fake_socket.has_pending_frames())
186
187    async def stop_receive_loop(self, connection_task):
188        connection_task.cancel()
189        with contextlib.suppress(asyncio.CancelledError):
190            await connection_task
191
192    async def test_failing_handler_does_not_stop_delivery_to_remaining_handlers(self):
193        failing = RecordingHandler(error=RuntimeError("handler failure"))
194        healthy = RecordingHandler()
195        receive_errors = RecordingHandler()
196        connection_errors = RecordingHandler()
197        self.client.on(WebSocketEvents.receiveMessage, failing)
198        self.client.on(WebSocketEvents.receiveMessage, healthy)
199        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
200        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
201
202        fake_socket, connection_task = await self.start_receive_loop()
203        fake_socket.push("[/heartbeat]")
204        fake_socket.push("raw-message")
205        await self.wait_until_frames_delivered(fake_socket)
206        await self.stop_receive_loop(connection_task)
207
208        self.assertEqual(failing.calls, [("raw-message",)])
209        self.assertEqual(healthy.calls, [("raw-message",)])
210        self.assertEqual(len(receive_errors.calls), 1)
211        self.assertEqual(len(receive_errors.calls[0]), 1)
212        self.assertIs(receive_errors.calls[0][0], failing.error)
213        self.assertEqual(connection_errors.calls, [])
214
215    async def test_failing_handler_without_error_observers_does_not_stop_reception(self):
216        failing = RecordingHandler(error=RuntimeError("handler failure"))
217        healthy = RecordingHandler()
218        self.client.on(WebSocketEvents.receiveMessage, failing)
219        self.client.on(WebSocketEvents.receiveMessage, healthy)
220
221        fake_socket, connection_task = await self.start_receive_loop()
222        fake_socket.push("[/heartbeat]")
223        fake_socket.push("raw-message")
224        await self.wait_until_frames_delivered(fake_socket)
225        await self.stop_receive_loop(connection_task)
226
227        self.assertEqual(healthy.calls, [("raw-message",)])
228
229    async def test_future_messages_continue_and_failed_handler_remains_registered(self):
230        failing = RecordingHandler(error=RuntimeError("handler failure"))
231        healthy = RecordingHandler()
232        receive_errors = RecordingHandler()
233        self.client.on(WebSocketEvents.receiveMessage, failing)
234        self.client.on(WebSocketEvents.receiveMessage, healthy)
235        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
236
237        fake_socket, connection_task = await self.start_receive_loop()
238        fake_socket.push("[/heartbeat]")
239        fake_socket.push("first-message")
240        fake_socket.push("second-message")
241        await self.wait_until_frames_delivered(fake_socket)
242        await self.stop_receive_loop(connection_task)
243
244        self.assertEqual(failing.calls, [("first-message",), ("second-message",)])
245        self.assertEqual(healthy.calls, [("first-message",), ("second-message",)])
246        self.assertEqual(
247            [call[0] for call in receive_errors.calls],
248            [failing.error, failing.error],
249        )
250
251    async def test_each_failing_handler_produces_its_own_error_event(self):
252        first = RecordingHandler(error=RuntimeError("first failure"))
253        second = RecordingHandler(error=ValueError("second failure"))
254        healthy = RecordingHandler()
255        receive_errors = RecordingHandler()
256        self.client.on(WebSocketEvents.receiveMessage, first)
257        self.client.on(WebSocketEvents.receiveMessage, healthy)
258        self.client.on(WebSocketEvents.receiveMessage, second)
259        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
260
261        fake_socket, connection_task = await self.start_receive_loop()
262        fake_socket.push("[/heartbeat]")
263        fake_socket.push("raw-message")
264        await self.wait_until_frames_delivered(fake_socket)
265        await self.stop_receive_loop(connection_task)
266
267        self.assertEqual(first.calls, [("raw-message",)])
268        self.assertEqual(second.calls, [("raw-message",)])
269        self.assertEqual(healthy.calls, [("raw-message",)])
270        self.assertEqual(
271            [call[0] for call in receive_errors.calls],
272            [first.error, second.error],
273        )
274
275    async def test_subscription_parser_failures_are_isolated_and_reception_continues(self):
276        raw_messages = RecordingHandler()
277        receive_errors = RecordingHandler()
278        notifications = RecordingHandler()
279        subscription_created = RecordingHandler()
280        connection_errors = RecordingHandler()
281        connection_created = RecordingHandler()
282        self.client.on(WebSocketEvents.receiveMessage, raw_messages)
283        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
284        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
285        self.client.on(WebSocketEvents.subscriptionCreated, subscription_created)
286        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
287        self.client.on(WebSocketEvents.connectionCreated, connection_created)
288
289        fake_socket, connection_task = await self.start_receive_loop()
290        fake_socket.push("[/heartbeat]")
291        await self.wait_until(lambda: len(connection_created.calls) == 1)
292
293        await self.client.create_subscription(events=EVENT_FILTERS)
294
295        request = json.loads(fake_socket.sent[0])
296        creation_frame = subscription_creation_frame(request[0]["messageId"])
297        notification_frame = server_notification_frame()
298        fake_socket.push(creation_frame)
299        fake_socket.push("not-json")
300        fake_socket.push("{}")
301        fake_socket.push("[]")
302        fake_socket.push(notification_frame)
303        await self.wait_until_frames_delivered(fake_socket)
304        await self.stop_receive_loop(connection_task)
305
306        self.assertEqual(len(subscription_created.calls), 1)
307        subscription = subscription_created.calls[0][0]
308        self.assertEqual(subscription.get_subscription_info()[1]["id"], "fake-subscription")
309
310        self.assertEqual(raw_messages.calls, [
311            (creation_frame,),
312            ("not-json",),
313            ("{}",),
314            ("[]",),
315            (notification_frame,),
316        ])
317        self.assertEqual(len(receive_errors.calls), 3)
318        self.assertIsInstance(receive_errors.calls[0][0], json.JSONDecodeError)
319        self.assertIsInstance(receive_errors.calls[1][0], KeyError)
320        self.assertIsInstance(receive_errors.calls[2][0], IndexError)
321        self.assertEqual(len(notifications.calls), 1)
322        self.assertEqual(notifications.calls[0][0], json.loads(notification_frame))
323        self.assertEqual(connection_errors.calls, [])
324
325    async def test_failing_error_observer_does_not_block_other_error_observers_or_reception(self):
326        failing_receive = RecordingHandler(error=RuntimeError("handler failure"))
327        healthy_receive = RecordingHandler()
328        failing_error_observer = RecordingHandler(error=RuntimeError("observer failure"))
329        recording_error_observer = RecordingHandler()
330        self.client.on(WebSocketEvents.receiveMessage, failing_receive)
331        self.client.on(WebSocketEvents.receiveMessage, healthy_receive)
332        self.client.on(WebSocketEvents.receiveMessageError, failing_error_observer)
333        self.client.on(WebSocketEvents.receiveMessageError, recording_error_observer)
334
335        fake_socket, connection_task = await self.start_receive_loop()
336        fake_socket.push("[/heartbeat]")
337        fake_socket.push("first-message")
338        fake_socket.push("second-message")
339        await self.wait_until_frames_delivered(fake_socket)
340        await self.stop_receive_loop(connection_task)
341
342        self.assertEqual(recording_error_observer.calls, [
343            (failing_receive.error,),
344            (failing_receive.error,),
345        ])
346        self.assertEqual(failing_receive.calls, [("first-message",), ("second-message",)])
347        self.assertEqual(healthy_receive.calls, [("first-message",), ("second-message",)])
348
349    async def test_unrelated_event_dispatch_semantics_remain_unchanged(self):
350        failing = RecordingHandler(error=RuntimeError("unrelated failure"))
351        healthy = RecordingHandler()
352        self.client.on(WebSocketEvents.subscriptionCreated, failing)
353        self.client.on(WebSocketEvents.subscriptionCreated, healthy)
354
355        with self.assertRaises(RuntimeError):
356            self.client.trigger(WebSocketEvents.subscriptionCreated, self.client)
357
358        self.assertEqual(failing.calls, [(self.client,)])
359        self.assertEqual(healthy.calls, [])
360
361    async def test_create_subscription_requires_events_argument(self):
362        fake_socket, connection_task = await self.start_receive_loop()
363        fake_socket.push("[/heartbeat]")
364        await self.wait_until(lambda: self.client._is_ready)
365
366        with self.assertRaises(TypeError):
367            await self.client.create_subscription()
368
369        await self.stop_receive_loop(connection_task)
370        self.assertEqual(fake_socket.sent, [])
371
372    async def test_create_subscription_rejects_none_and_empty_events(self):
373        creation_errors = RecordingHandler()
374        created = RecordingHandler()
375        notifications = RecordingHandler()
376        self.client.on(WebSocketEvents.createSubscriptionError, creation_errors)
377        self.client.on(WebSocketEvents.subscriptionCreated, created)
378        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
379
380        fake_socket, connection_task = await self.start_receive_loop()
381        fake_socket.push("[/heartbeat]")
382        await self.wait_until(lambda: self.client._is_ready)
383
384        for invalid_events in (None, []):
385            with self.assertRaises(Exception) as ctx:
386                await self.client.create_subscription(events=invalid_events)
387            self.assertEqual(str(ctx.exception), "Events are undefined")
388
389        fake_socket.push(server_notification_frame())
390        await self.wait_until_frames_delivered(fake_socket)
391
392        self.assertEqual(fake_socket.sent, [])
393        self.assertEqual(created.calls, [])
394        self.assertEqual(len(creation_errors.calls), 2)
395        for call in creation_errors.calls:
396            self.assertEqual(str(call[0]), "Events are undefined")
397        self.assertEqual(notifications.calls, [])
398        await self.stop_receive_loop(connection_task)
399
400    async def test_second_creation_while_first_is_pending_is_rejected(self):
401        creation_errors = RecordingHandler()
402        created = RecordingHandler()
403        notifications = RecordingHandler()
404        self.client.on(WebSocketEvents.createSubscriptionError, creation_errors)
405        self.client.on(WebSocketEvents.subscriptionCreated, created)
406        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
407
408        fake_socket = GatedSendWebSocket()
409        connection_task = await self.start_receive_loop_with(fake_socket)
410
411        first_task = asyncio.create_task(self.client.create_subscription(events=EVENT_FILTERS))
412        await self.wait_until(lambda: fake_socket.send_started.is_set())
413
414        with self.assertRaises(Exception) as ctx:
415            await self.client.create_subscription(events=OTHER_FILTERS)
416        self.assertEqual(
417            str(ctx.exception),
418            "WebSocket subscription creation is already in progress; "
419            "wait for subscriptionCreated or createSubscriptionError before retrying",
420        )
421        self.assertEqual(len(creation_errors.calls), 1)
422        self.assertEqual(str(creation_errors.calls[0][0]), str(ctx.exception))
423
424        fake_socket.send_gate.set()
425        await first_task
426
427        self.assertEqual(len(fake_socket.sent), 1)
428        request = json.loads(fake_socket.sent[0])
429        self.assertEqual(request[0]["method"], "POST")
430        self.assertEqual(request[1]["eventFilters"], EVENT_FILTERS)
431
432        fake_socket.push(subscription_creation_frame(request[0]["messageId"]))
433        await self.wait_until(lambda: len(created.calls) == 1)
434
435        fake_socket.push(server_notification_frame())
436        await self.wait_until_frames_delivered(fake_socket)
437        self.assertEqual(len(notifications.calls), 1)
438
439        await self.stop_receive_loop(connection_task)
440        self.assertEqual(len(created.calls), 1)
441        self.assertEqual(len(creation_errors.calls), 1)
442
443    async def test_second_creation_after_confirmed_creation_is_rejected(self):
444        creation_errors = RecordingHandler()
445        created = RecordingHandler()
446        updated = RecordingHandler()
447        notifications = RecordingHandler()
448        self.client.on(WebSocketEvents.createSubscriptionError, creation_errors)
449        self.client.on(WebSocketEvents.subscriptionCreated, created)
450        self.client.on(WebSocketEvents.subscriptionUpdated, updated)
451        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
452
453        fake_socket, connection_task = await self.start_receive_loop()
454        fake_socket.push("[/heartbeat]")
455        await self.wait_until(lambda: self.client._is_ready)
456
457        await self.client.create_subscription(events=EVENT_FILTERS)
458        await self.deliver_creation_response(fake_socket)
459        await self.wait_until(lambda: len(created.calls) == 1)
460        subscription = created.calls[0][0]
461
462        with self.assertRaises(Exception) as ctx:
463            await self.client.create_subscription(events=OTHER_FILTERS)
464        self.assertEqual(
465            str(ctx.exception),
466            "A WebSocket subscription already exists; use update_subscription() "
467            "to change its events or remove_subscription() before creating another",
468        )
469        self.assertEqual(len(creation_errors.calls), 1)
470        self.assertEqual(str(creation_errors.calls[0][0]), str(ctx.exception))
471
472        self.assertEqual(len(fake_socket.sent), 1)
473        self.assertEqual(json.loads(fake_socket.sent[0])[0]["method"], "POST")
474        self.assertEqual(len(created.calls), 1)
475        self.assertEqual(updated.calls, [])
476        self.assertEqual(subscription.get_subscription_info()[1]["id"], "fake-subscription")
477
478        fake_socket.push(server_notification_frame())
479        await self.wait_until_frames_delivered(fake_socket)
480        self.assertEqual(len(notifications.calls), 1)
481
482        await self.stop_receive_loop(connection_task)
483
484    async def test_retry_after_send_failure_uses_new_events_without_duplicate_listeners(self):
485        creation_errors = RecordingHandler()
486        send_errors = RecordingHandler()
487        created = RecordingHandler()
488        notifications = RecordingHandler()
489        self.client.on(WebSocketEvents.createSubscriptionError, creation_errors)
490        self.client.on(WebSocketEvents.sendMessageError, send_errors)
491        self.client.on(WebSocketEvents.subscriptionCreated, created)
492        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
493
494        fake_socket = FlakySendWebSocket(RuntimeError("send failure"))
495        connection_task = await self.start_receive_loop_with(fake_socket)
496
497        with self.assertRaises(Exception):
498            await self.client.create_subscription(events=EVENT_FILTERS)
499        self.assertEqual(len(send_errors.calls), 1)
500        self.assertEqual(len(creation_errors.calls), 1)
501        self.assertEqual(fake_socket.sent, [])
502
503        await self.client.create_subscription(events=OTHER_FILTERS)
504
505        self.assertEqual(len(fake_socket.sent), 1)
506        request = json.loads(fake_socket.sent[0])
507        self.assertEqual(request[1]["eventFilters"], OTHER_FILTERS)
508        fake_socket.push(subscription_creation_frame(request[0]["messageId"]))
509        await self.wait_until(lambda: len(created.calls) == 1)
510
511        fake_socket.push(server_notification_frame())
512        await self.wait_until_frames_delivered(fake_socket)
513        self.assertEqual(len(notifications.calls), 1)
514
515        await self.stop_receive_loop(connection_task)
516        self.assertEqual(len(created.calls), 1)
517
518    async def test_retry_after_rejected_creation_succeeds_without_duplicate_listeners(self):
519        creation_errors = RecordingHandler()
520        created = RecordingHandler()
521        notifications = RecordingHandler()
522        self.client.on(WebSocketEvents.createSubscriptionError, creation_errors)
523        self.client.on(WebSocketEvents.subscriptionCreated, created)
524        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
525
526        fake_socket, connection_task = await self.start_receive_loop()
527        fake_socket.push("[/heartbeat]")
528        await self.wait_until(lambda: self.client._is_ready)
529
530        await self.client.create_subscription(events=EVENT_FILTERS)
531        await self.deliver_creation_response(fake_socket, status=403)
532        await self.wait_until(lambda: len(creation_errors.calls) == 1)
533        self.assertEqual(
534            str(creation_errors.calls[0][0]),
535            "WebSocket subscription creation failed with status 403",
536        )
537        self.assertEqual(created.calls, [])
538
539        await self.client.create_subscription(events=OTHER_FILTERS)
540
541        self.assertEqual(len(fake_socket.sent), 2)
542        retry_request = json.loads(fake_socket.sent[1])
543        self.assertEqual(retry_request[0]["method"], "POST")
544        self.assertEqual(retry_request[1]["eventFilters"], OTHER_FILTERS)
545        fake_socket.push(subscription_creation_frame(retry_request[0]["messageId"]))
546        await self.wait_until(lambda: len(created.calls) == 1)
547
548        fake_socket.push(server_notification_frame())
549        await self.wait_until_frames_delivered(fake_socket)
550        self.assertEqual(len(notifications.calls), 1)
551
552        await self.stop_receive_loop(connection_task)
553        self.assertEqual(len(created.calls), 1)
554        self.assertEqual(len(creation_errors.calls), 1)
555
556    async def test_creation_after_removal_reuses_client_subscription_without_duplicate_listeners(self):
557        created = RecordingHandler()
558        removed = RecordingHandler()
559        notifications = RecordingHandler()
560        self.client.on(WebSocketEvents.subscriptionCreated, created)
561        self.client.on(WebSocketEvents.subscriptionRemoved, removed)
562        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
563
564        fake_socket, connection_task = await self.start_receive_loop()
565        fake_socket.push("[/heartbeat]")
566        await self.wait_until(lambda: self.client._is_ready)
567
568        await self.client.create_subscription(events=EVENT_FILTERS)
569        await self.deliver_creation_response(fake_socket)
570        await self.wait_until(lambda: len(created.calls) == 1)
571        subscription = created.calls[0][0]
572
573        await self.client.remove_subscription(subscription)
574        removal_request = json.loads(fake_socket.sent[1])
575        self.assertEqual(removal_request[0]["method"], "DELETE")
576        fake_socket.push(subscription_removal_frame(removal_request[0]["messageId"]))
577        await self.wait_until(lambda: len(removed.calls) == 1)
578
579        fake_socket.push(server_notification_frame())
580        await self.wait_until_frames_delivered(fake_socket)
581        self.assertEqual(notifications.calls, [])
582
583        await self.client.create_subscription(events=OTHER_FILTERS)
584
585        self.assertEqual(len(fake_socket.sent), 3)
586        recreated_request = json.loads(fake_socket.sent[2])
587        self.assertEqual(recreated_request[0]["method"], "POST")
588        self.assertEqual(recreated_request[1]["eventFilters"], OTHER_FILTERS)
589        fake_socket.push(subscription_creation_frame(recreated_request[0]["messageId"]))
590        await self.wait_until(lambda: len(created.calls) == 2)
591
592        fake_socket.push(server_notification_frame())
593        await self.wait_until_frames_delivered(fake_socket)
594        self.assertEqual(len(notifications.calls), 1)
595
596        await self.stop_receive_loop(connection_task)
597        self.assertEqual(len(created.calls), 2)
598        self.assertEqual(len(removed.calls), 1)
599
600    async def test_successful_creation_returns_none_and_delivers_subscription_via_event(self):
601        created = RecordingHandler()
602        self.client.on(WebSocketEvents.subscriptionCreated, created)
603
604        fake_socket, connection_task = await self.start_receive_loop()
605        fake_socket.push("[/heartbeat]")
606        await self.wait_until(lambda: self.client._is_ready)
607
608        result = await self.client.create_subscription(events=EVENT_FILTERS)
609        self.assertIsNone(result)
610
611        await self.deliver_creation_response(fake_socket)
612        await self.wait_until(lambda: len(created.calls) == 1)
613
614        self.assertEqual(len(created.calls), 1)
615        subscription = created.calls[0][0]
616        self.assertEqual(subscription.get_subscription_info()[1]["id"], "fake-subscription")
617
618        await self.stop_receive_loop(connection_task)
619
620    async def test_recv_failure_after_handshake_reports_receive_error_and_cleans_up(self):
621        receive_error = RuntimeError("receive failure")
622        receive_errors = RecordingHandler()
623        connection_errors = RecordingHandler()
624        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
625        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
626
627        sockets = []
628
629        def on_connected(*_):
630            sockets[0].fail_next_receive(receive_error)
631
632        self.client.on(WebSocketEvents.connectionCreated, on_connected)
633
634        fake_socket, connection_task = await self.start_receive_loop()
635        fake_socket.push("[/heartbeat]")
636        sockets.append(fake_socket)
637
638        result = await connection_task
639
640        self.assertIsNone(result)
641        self.assertEqual(len(receive_errors.calls), 1)
642        self.assertEqual(len(receive_errors.calls[0]), 1)
643        self.assertIs(receive_errors.calls[0][0], receive_error)
644        self.assertEqual(connection_errors.calls, [])
645        self.assertFalse(self.client._is_ready)
646        await self.wait_until(lambda: self.client._heartbeat_task.done())
647        self.assertTrue(self.client._heartbeat_task.cancelled())
648
649    async def test_receive_loop_cancellation_marks_client_not_ready_and_cancels_heartbeat(self):
650        receive_errors = RecordingHandler()
651        connection_errors = RecordingHandler()
652        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
653        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
654
655        fake_socket, connection_task = await self.start_receive_loop()
656        fake_socket.push("[/heartbeat]")
657        await self.wait_until(lambda: self.client._is_ready)
658        self.assertTrue(self.client._is_ready)
659
660        await self.stop_receive_loop(connection_task)
661
662        self.assertFalse(self.client._is_ready)
663        await self.wait_until(lambda: self.client._heartbeat_task.done())
664        self.assertTrue(self.client._heartbeat_task.cancelled())
665        self.assertEqual(receive_errors.calls, [])
666        self.assertEqual(connection_errors.calls, [])
667
668    async def test_intentional_close_performs_cleanup_without_receive_error(self):
669        receive_errors = RecordingHandler()
670        connection_errors = RecordingHandler()
671        close_errors = RecordingHandler()
672        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
673        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
674        self.client.on(WebSocketEvents.closeConnectionError, close_errors)
675
676        fake_socket, connection_task = await self.start_receive_loop()
677        fake_socket.push("[/heartbeat]")
678        await self.wait_until(lambda: self.client._is_ready)
679
680        await self.client.close_connection()
681        result = await connection_task
682
683        self.assertIsNone(result)
684        self.assertTrue(fake_socket.closed)
685        self.assertFalse(self.client._is_ready)
686        await self.wait_until(lambda: self.client._heartbeat_task.done())
687        self.assertTrue(self.client._heartbeat_task.cancelled())
688        self.assertEqual(receive_errors.calls, [])
689        self.assertEqual(connection_errors.calls, [])
690        self.assertEqual(close_errors.calls, [])
691
692    async def test_receive_failure_on_reconnected_client_is_reported_after_intentional_close(self):
693        receive_error = RuntimeError("receive failure after reconnect")
694        receive_errors = RecordingHandler()
695        connection_errors = RecordingHandler()
696        close_errors = RecordingHandler()
697        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
698        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
699        self.client.on(WebSocketEvents.closeConnectionError, close_errors)
700
701        sockets = []
702
703        def fail_latest_socket(*_):
704            sockets[-1].fail_next_receive(receive_error)
705
706        self.client.on(WebSocketEvents.connectionCreated, fail_latest_socket)
707
708        first_socket, connection_task = await self.start_receive_loop()
709        sockets.append(first_socket)
710        first_socket.push("[/heartbeat]")
711        await self.wait_until(lambda: self.client._is_ready)
712
713        await self.client.close_connection()
714        await connection_task
715        self.assertTrue(first_socket.closed)
716
717        second_socket = FakeWebSocket()
718        sockets.append(second_socket)
719
720        async def connect_again(uri):
721            return second_socket
722
723        with mock.patch("websockets.connect", connect_again):
724            recovery_task = asyncio.create_task(self.client.recover_connection())
725            second_socket.push("[/heartbeat]")
726            await self.wait_until(lambda: self.client._is_ready)
727            result = await recovery_task
728
729        self.assertIsNone(result)
730        self.assertEqual(len(receive_errors.calls), 1)
731        self.assertEqual(len(receive_errors.calls[0]), 1)
732        self.assertIs(receive_errors.calls[0][0], receive_error)
733        self.assertEqual(connection_errors.calls, [])
734        self.assertEqual(close_errors.calls, [])
735        self.assertFalse(self.client._is_ready)
736        await self.wait_until(lambda: self.client._heartbeat_task.done())
737        self.assertTrue(self.client._heartbeat_task.cancelled())
738
739    async def test_connection_establishment_failure_retains_create_connection_error(self):
740        connect_error = RuntimeError("connect failure")
741        receive_errors = RecordingHandler()
742        connection_errors = RecordingHandler()
743        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
744        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
745
746        async def connect(uri):
747            raise connect_error
748
749        with mock.patch("websockets.connect", connect):
750            with self.assertRaises(RuntimeError):
751                await self.client.create_new_connection()
752
753        self.assertTrue(connection_errors.calls)
754        for call in connection_errors.calls:
755            self.assertIs(call[0], connect_error)
756        self.assertEqual(receive_errors.calls, [])
757
758    async def test_initial_handshake_failure_retains_create_connection_error(self):
759        handshake_error = RuntimeError("handshake failure")
760        receive_errors = RecordingHandler()
761        connection_errors = RecordingHandler()
762        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
763        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
764
765        fake_socket = FakeWebSocket()
766        fake_socket.fail_next_receive(handshake_error)
767
768        async def connect(uri):
769            return fake_socket
770
771        with mock.patch("websockets.connect", connect):
772            with self.assertRaises(RuntimeError):
773                await self.client.create_new_connection()
774
775        self.assertTrue(connection_errors.calls)
776        for call in connection_errors.calls:
777            self.assertIs(call[0], handshake_error)
778        self.assertEqual(receive_errors.calls, [])
779        self.assertFalse(self.client._is_ready)
780        self.assertIsNone(self.client._heartbeat_task)
781
782
783if __name__ == "__main__":
784    unittest.main()
class FakeResponse:
13class FakeResponse:
14    def __init__(self, payload):
15        self._payload = payload
16
17    def json_dict(self):
18        return self._payload
FakeResponse(payload)
14    def __init__(self, payload):
15        self._payload = payload
def json_dict(self):
17    def json_dict(self):
18        return self._payload
class FakePlatform:
21class FakePlatform:
22    def post(self, path, body=None):
23        return FakeResponse({
24            "uri": "wss://fake-websocket",
25            "ws_access_token": "fake-ws-token",
26        })
def post(self, path, body=None):
22    def post(self, path, body=None):
23        return FakeResponse({
24            "uri": "wss://fake-websocket",
25            "ws_access_token": "fake-ws-token",
26        })
class FakeWebSocket:
29class FakeWebSocket:
30    def __init__(self):
31        self.sent = []
32        self.received = []
33        self.closed = False
34        self._fail_next_recv = False
35        self._receive_error = None
36        self._frames = asyncio.Queue()
37
38    def fail_next_receive(self, error):
39        self._fail_next_recv = True
40        self._receive_error = error
41
42    def push(self, frame):
43        self._frames.put_nowait(frame)
44
45    def has_pending_frames(self):
46        return self._frames.qsize() > 0
47
48    async def recv(self):
49        if self._fail_next_recv:
50            self._fail_next_recv = False
51            raise self._receive_error
52        frame = await self._frames.get()
53        if frame is None:
54            raise ConnectionResetError("connection closed")
55        self.received.append(frame)
56        return frame
57
58    async def send(self, message):
59        self.sent.append(message)
60
61    async def close(self):
62        self.closed = True
63        self._frames.put_nowait(None)
sent
received
closed
def fail_next_receive(self, error):
38    def fail_next_receive(self, error):
39        self._fail_next_recv = True
40        self._receive_error = error
def push(self, frame):
42    def push(self, frame):
43        self._frames.put_nowait(frame)
def has_pending_frames(self):
45    def has_pending_frames(self):
46        return self._frames.qsize() > 0
async def recv(self):
48    async def recv(self):
49        if self._fail_next_recv:
50            self._fail_next_recv = False
51            raise self._receive_error
52        frame = await self._frames.get()
53        if frame is None:
54            raise ConnectionResetError("connection closed")
55        self.received.append(frame)
56        return frame
async def send(self, message):
58    async def send(self, message):
59        self.sent.append(message)
async def close(self):
61    async def close(self):
62        self.closed = True
63        self._frames.put_nowait(None)
class GatedSendWebSocket(FakeWebSocket):
66class GatedSendWebSocket(FakeWebSocket):
67    def __init__(self):
68        super().__init__()
69        self.send_started = asyncio.Event()
70        self.send_gate = asyncio.Event()
71
72    async def send(self, message):
73        self.send_started.set()
74        await self.send_gate.wait()
75        await FakeWebSocket.send(self, message)
send_started
send_gate
async def send(self, message):
72    async def send(self, message):
73        self.send_started.set()
74        await self.send_gate.wait()
75        await FakeWebSocket.send(self, message)
class FlakySendWebSocket(FakeWebSocket):
78class FlakySendWebSocket(FakeWebSocket):
79    def __init__(self, error):
80        super().__init__()
81        self._send_error = error
82
83    async def send(self, message):
84        if self._send_error is not None:
85            error = self._send_error
86            self._send_error = None
87            raise error
88        await FakeWebSocket.send(self, message)
FlakySendWebSocket(error)
79    def __init__(self, error):
80        super().__init__()
81        self._send_error = error
async def send(self, message):
83    async def send(self, message):
84        if self._send_error is not None:
85            error = self._send_error
86            self._send_error = None
87            raise error
88        await FakeWebSocket.send(self, message)
class RecordingHandler:
91class RecordingHandler:
92    def __init__(self, error=None):
93        self.calls = []
94        self.error = error
95
96    def __call__(self, *args):
97        self.calls.append(args)
98        if self.error is not None:
99            raise self.error
RecordingHandler(error=None)
92    def __init__(self, error=None):
93        self.calls = []
94        self.error = error
calls
error
EVENT_FILTERS = ['/restapi/v1.0/account/~/extension/~/presence']
OTHER_FILTERS = ['/restapi/v1.0/account/~/extension/~/message-store']
def subscription_creation_frame(message_id, status=200):
106def subscription_creation_frame(message_id, status=200):
107    return json.dumps([
108        {
109            "type": "ClientRequest",
110            "messageId": message_id,
111            "status": status,
112        },
113        {
114            "uri": "/restapi/v1.0/subscription/fake-subscription",
115            "id": "fake-subscription",
116            "status": "Active",
117            "eventFilters": EVENT_FILTERS,
118            "deliveryMode": {"transportType": "WebSocket"},
119        },
120    ])
def subscription_removal_frame(message_id):
123def subscription_removal_frame(message_id):
124    return json.dumps([
125        {
126            "type": "ClientRequest",
127            "messageId": message_id,
128            "status": 200,
129        },
130    ])
def server_notification_frame():
133def server_notification_frame():
134    return json.dumps([
135        {
136            "type": "ServerNotification",
137            "messageId": str(uuid.uuid4()),
138        },
139        {
140            "uri": "/restapi/v1.0/subscription/fake-subscription",
141            "event": {"/restapi/v1.0/account/~/extension/~/presence": {"activeCalls": []}},
142        },
143    ])
class WebSocketClientTest(unittest.async_case.IsolatedAsyncioTestCase):
146class WebSocketClientTest(unittest.IsolatedAsyncioTestCase):
147    def setUp(self):
148        self.client = WebSocketClient(FakePlatform())
149
150    async def start_receive_loop(self):
151        fake_socket = FakeWebSocket()
152
153        async def connect(uri):
154            return fake_socket
155
156        patcher = mock.patch("websockets.connect", connect)
157        patcher.start()
158        self.addCleanup(patcher.stop)
159        connection_task = asyncio.create_task(self.client.create_new_connection())
160        return fake_socket, connection_task
161
162    async def start_receive_loop_with(self, fake_socket):
163        async def connect(uri):
164            return fake_socket
165
166        patcher = mock.patch("websockets.connect", connect)
167        patcher.start()
168        self.addCleanup(patcher.stop)
169        connection_task = asyncio.create_task(self.client.create_new_connection())
170        fake_socket.push("[/heartbeat]")
171        await self.wait_until(lambda: self.client._is_ready)
172        return connection_task
173
174    async def deliver_creation_response(self, fake_socket, status=200):
175        request = json.loads(fake_socket.sent[0])
176        fake_socket.push(subscription_creation_frame(request[0]["messageId"], status=status))
177
178    async def wait_until(self, predicate, limit=100):
179        for _ in range(limit):
180            if predicate():
181                return
182            await asyncio.sleep(0)
183        self.fail("condition was not met before the spin limit")
184
185    async def wait_until_frames_delivered(self, fake_socket):
186        await self.wait_until(lambda: not fake_socket.has_pending_frames())
187
188    async def stop_receive_loop(self, connection_task):
189        connection_task.cancel()
190        with contextlib.suppress(asyncio.CancelledError):
191            await connection_task
192
193    async def test_failing_handler_does_not_stop_delivery_to_remaining_handlers(self):
194        failing = RecordingHandler(error=RuntimeError("handler failure"))
195        healthy = RecordingHandler()
196        receive_errors = RecordingHandler()
197        connection_errors = RecordingHandler()
198        self.client.on(WebSocketEvents.receiveMessage, failing)
199        self.client.on(WebSocketEvents.receiveMessage, healthy)
200        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
201        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
202
203        fake_socket, connection_task = await self.start_receive_loop()
204        fake_socket.push("[/heartbeat]")
205        fake_socket.push("raw-message")
206        await self.wait_until_frames_delivered(fake_socket)
207        await self.stop_receive_loop(connection_task)
208
209        self.assertEqual(failing.calls, [("raw-message",)])
210        self.assertEqual(healthy.calls, [("raw-message",)])
211        self.assertEqual(len(receive_errors.calls), 1)
212        self.assertEqual(len(receive_errors.calls[0]), 1)
213        self.assertIs(receive_errors.calls[0][0], failing.error)
214        self.assertEqual(connection_errors.calls, [])
215
216    async def test_failing_handler_without_error_observers_does_not_stop_reception(self):
217        failing = RecordingHandler(error=RuntimeError("handler failure"))
218        healthy = RecordingHandler()
219        self.client.on(WebSocketEvents.receiveMessage, failing)
220        self.client.on(WebSocketEvents.receiveMessage, healthy)
221
222        fake_socket, connection_task = await self.start_receive_loop()
223        fake_socket.push("[/heartbeat]")
224        fake_socket.push("raw-message")
225        await self.wait_until_frames_delivered(fake_socket)
226        await self.stop_receive_loop(connection_task)
227
228        self.assertEqual(healthy.calls, [("raw-message",)])
229
230    async def test_future_messages_continue_and_failed_handler_remains_registered(self):
231        failing = RecordingHandler(error=RuntimeError("handler failure"))
232        healthy = RecordingHandler()
233        receive_errors = RecordingHandler()
234        self.client.on(WebSocketEvents.receiveMessage, failing)
235        self.client.on(WebSocketEvents.receiveMessage, healthy)
236        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
237
238        fake_socket, connection_task = await self.start_receive_loop()
239        fake_socket.push("[/heartbeat]")
240        fake_socket.push("first-message")
241        fake_socket.push("second-message")
242        await self.wait_until_frames_delivered(fake_socket)
243        await self.stop_receive_loop(connection_task)
244
245        self.assertEqual(failing.calls, [("first-message",), ("second-message",)])
246        self.assertEqual(healthy.calls, [("first-message",), ("second-message",)])
247        self.assertEqual(
248            [call[0] for call in receive_errors.calls],
249            [failing.error, failing.error],
250        )
251
252    async def test_each_failing_handler_produces_its_own_error_event(self):
253        first = RecordingHandler(error=RuntimeError("first failure"))
254        second = RecordingHandler(error=ValueError("second failure"))
255        healthy = RecordingHandler()
256        receive_errors = RecordingHandler()
257        self.client.on(WebSocketEvents.receiveMessage, first)
258        self.client.on(WebSocketEvents.receiveMessage, healthy)
259        self.client.on(WebSocketEvents.receiveMessage, second)
260        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
261
262        fake_socket, connection_task = await self.start_receive_loop()
263        fake_socket.push("[/heartbeat]")
264        fake_socket.push("raw-message")
265        await self.wait_until_frames_delivered(fake_socket)
266        await self.stop_receive_loop(connection_task)
267
268        self.assertEqual(first.calls, [("raw-message",)])
269        self.assertEqual(second.calls, [("raw-message",)])
270        self.assertEqual(healthy.calls, [("raw-message",)])
271        self.assertEqual(
272            [call[0] for call in receive_errors.calls],
273            [first.error, second.error],
274        )
275
276    async def test_subscription_parser_failures_are_isolated_and_reception_continues(self):
277        raw_messages = RecordingHandler()
278        receive_errors = RecordingHandler()
279        notifications = RecordingHandler()
280        subscription_created = RecordingHandler()
281        connection_errors = RecordingHandler()
282        connection_created = RecordingHandler()
283        self.client.on(WebSocketEvents.receiveMessage, raw_messages)
284        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
285        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
286        self.client.on(WebSocketEvents.subscriptionCreated, subscription_created)
287        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
288        self.client.on(WebSocketEvents.connectionCreated, connection_created)
289
290        fake_socket, connection_task = await self.start_receive_loop()
291        fake_socket.push("[/heartbeat]")
292        await self.wait_until(lambda: len(connection_created.calls) == 1)
293
294        await self.client.create_subscription(events=EVENT_FILTERS)
295
296        request = json.loads(fake_socket.sent[0])
297        creation_frame = subscription_creation_frame(request[0]["messageId"])
298        notification_frame = server_notification_frame()
299        fake_socket.push(creation_frame)
300        fake_socket.push("not-json")
301        fake_socket.push("{}")
302        fake_socket.push("[]")
303        fake_socket.push(notification_frame)
304        await self.wait_until_frames_delivered(fake_socket)
305        await self.stop_receive_loop(connection_task)
306
307        self.assertEqual(len(subscription_created.calls), 1)
308        subscription = subscription_created.calls[0][0]
309        self.assertEqual(subscription.get_subscription_info()[1]["id"], "fake-subscription")
310
311        self.assertEqual(raw_messages.calls, [
312            (creation_frame,),
313            ("not-json",),
314            ("{}",),
315            ("[]",),
316            (notification_frame,),
317        ])
318        self.assertEqual(len(receive_errors.calls), 3)
319        self.assertIsInstance(receive_errors.calls[0][0], json.JSONDecodeError)
320        self.assertIsInstance(receive_errors.calls[1][0], KeyError)
321        self.assertIsInstance(receive_errors.calls[2][0], IndexError)
322        self.assertEqual(len(notifications.calls), 1)
323        self.assertEqual(notifications.calls[0][0], json.loads(notification_frame))
324        self.assertEqual(connection_errors.calls, [])
325
326    async def test_failing_error_observer_does_not_block_other_error_observers_or_reception(self):
327        failing_receive = RecordingHandler(error=RuntimeError("handler failure"))
328        healthy_receive = RecordingHandler()
329        failing_error_observer = RecordingHandler(error=RuntimeError("observer failure"))
330        recording_error_observer = RecordingHandler()
331        self.client.on(WebSocketEvents.receiveMessage, failing_receive)
332        self.client.on(WebSocketEvents.receiveMessage, healthy_receive)
333        self.client.on(WebSocketEvents.receiveMessageError, failing_error_observer)
334        self.client.on(WebSocketEvents.receiveMessageError, recording_error_observer)
335
336        fake_socket, connection_task = await self.start_receive_loop()
337        fake_socket.push("[/heartbeat]")
338        fake_socket.push("first-message")
339        fake_socket.push("second-message")
340        await self.wait_until_frames_delivered(fake_socket)
341        await self.stop_receive_loop(connection_task)
342
343        self.assertEqual(recording_error_observer.calls, [
344            (failing_receive.error,),
345            (failing_receive.error,),
346        ])
347        self.assertEqual(failing_receive.calls, [("first-message",), ("second-message",)])
348        self.assertEqual(healthy_receive.calls, [("first-message",), ("second-message",)])
349
350    async def test_unrelated_event_dispatch_semantics_remain_unchanged(self):
351        failing = RecordingHandler(error=RuntimeError("unrelated failure"))
352        healthy = RecordingHandler()
353        self.client.on(WebSocketEvents.subscriptionCreated, failing)
354        self.client.on(WebSocketEvents.subscriptionCreated, healthy)
355
356        with self.assertRaises(RuntimeError):
357            self.client.trigger(WebSocketEvents.subscriptionCreated, self.client)
358
359        self.assertEqual(failing.calls, [(self.client,)])
360        self.assertEqual(healthy.calls, [])
361
362    async def test_create_subscription_requires_events_argument(self):
363        fake_socket, connection_task = await self.start_receive_loop()
364        fake_socket.push("[/heartbeat]")
365        await self.wait_until(lambda: self.client._is_ready)
366
367        with self.assertRaises(TypeError):
368            await self.client.create_subscription()
369
370        await self.stop_receive_loop(connection_task)
371        self.assertEqual(fake_socket.sent, [])
372
373    async def test_create_subscription_rejects_none_and_empty_events(self):
374        creation_errors = RecordingHandler()
375        created = RecordingHandler()
376        notifications = RecordingHandler()
377        self.client.on(WebSocketEvents.createSubscriptionError, creation_errors)
378        self.client.on(WebSocketEvents.subscriptionCreated, created)
379        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
380
381        fake_socket, connection_task = await self.start_receive_loop()
382        fake_socket.push("[/heartbeat]")
383        await self.wait_until(lambda: self.client._is_ready)
384
385        for invalid_events in (None, []):
386            with self.assertRaises(Exception) as ctx:
387                await self.client.create_subscription(events=invalid_events)
388            self.assertEqual(str(ctx.exception), "Events are undefined")
389
390        fake_socket.push(server_notification_frame())
391        await self.wait_until_frames_delivered(fake_socket)
392
393        self.assertEqual(fake_socket.sent, [])
394        self.assertEqual(created.calls, [])
395        self.assertEqual(len(creation_errors.calls), 2)
396        for call in creation_errors.calls:
397            self.assertEqual(str(call[0]), "Events are undefined")
398        self.assertEqual(notifications.calls, [])
399        await self.stop_receive_loop(connection_task)
400
401    async def test_second_creation_while_first_is_pending_is_rejected(self):
402        creation_errors = RecordingHandler()
403        created = RecordingHandler()
404        notifications = RecordingHandler()
405        self.client.on(WebSocketEvents.createSubscriptionError, creation_errors)
406        self.client.on(WebSocketEvents.subscriptionCreated, created)
407        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
408
409        fake_socket = GatedSendWebSocket()
410        connection_task = await self.start_receive_loop_with(fake_socket)
411
412        first_task = asyncio.create_task(self.client.create_subscription(events=EVENT_FILTERS))
413        await self.wait_until(lambda: fake_socket.send_started.is_set())
414
415        with self.assertRaises(Exception) as ctx:
416            await self.client.create_subscription(events=OTHER_FILTERS)
417        self.assertEqual(
418            str(ctx.exception),
419            "WebSocket subscription creation is already in progress; "
420            "wait for subscriptionCreated or createSubscriptionError before retrying",
421        )
422        self.assertEqual(len(creation_errors.calls), 1)
423        self.assertEqual(str(creation_errors.calls[0][0]), str(ctx.exception))
424
425        fake_socket.send_gate.set()
426        await first_task
427
428        self.assertEqual(len(fake_socket.sent), 1)
429        request = json.loads(fake_socket.sent[0])
430        self.assertEqual(request[0]["method"], "POST")
431        self.assertEqual(request[1]["eventFilters"], EVENT_FILTERS)
432
433        fake_socket.push(subscription_creation_frame(request[0]["messageId"]))
434        await self.wait_until(lambda: len(created.calls) == 1)
435
436        fake_socket.push(server_notification_frame())
437        await self.wait_until_frames_delivered(fake_socket)
438        self.assertEqual(len(notifications.calls), 1)
439
440        await self.stop_receive_loop(connection_task)
441        self.assertEqual(len(created.calls), 1)
442        self.assertEqual(len(creation_errors.calls), 1)
443
444    async def test_second_creation_after_confirmed_creation_is_rejected(self):
445        creation_errors = RecordingHandler()
446        created = RecordingHandler()
447        updated = RecordingHandler()
448        notifications = RecordingHandler()
449        self.client.on(WebSocketEvents.createSubscriptionError, creation_errors)
450        self.client.on(WebSocketEvents.subscriptionCreated, created)
451        self.client.on(WebSocketEvents.subscriptionUpdated, updated)
452        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
453
454        fake_socket, connection_task = await self.start_receive_loop()
455        fake_socket.push("[/heartbeat]")
456        await self.wait_until(lambda: self.client._is_ready)
457
458        await self.client.create_subscription(events=EVENT_FILTERS)
459        await self.deliver_creation_response(fake_socket)
460        await self.wait_until(lambda: len(created.calls) == 1)
461        subscription = created.calls[0][0]
462
463        with self.assertRaises(Exception) as ctx:
464            await self.client.create_subscription(events=OTHER_FILTERS)
465        self.assertEqual(
466            str(ctx.exception),
467            "A WebSocket subscription already exists; use update_subscription() "
468            "to change its events or remove_subscription() before creating another",
469        )
470        self.assertEqual(len(creation_errors.calls), 1)
471        self.assertEqual(str(creation_errors.calls[0][0]), str(ctx.exception))
472
473        self.assertEqual(len(fake_socket.sent), 1)
474        self.assertEqual(json.loads(fake_socket.sent[0])[0]["method"], "POST")
475        self.assertEqual(len(created.calls), 1)
476        self.assertEqual(updated.calls, [])
477        self.assertEqual(subscription.get_subscription_info()[1]["id"], "fake-subscription")
478
479        fake_socket.push(server_notification_frame())
480        await self.wait_until_frames_delivered(fake_socket)
481        self.assertEqual(len(notifications.calls), 1)
482
483        await self.stop_receive_loop(connection_task)
484
485    async def test_retry_after_send_failure_uses_new_events_without_duplicate_listeners(self):
486        creation_errors = RecordingHandler()
487        send_errors = RecordingHandler()
488        created = RecordingHandler()
489        notifications = RecordingHandler()
490        self.client.on(WebSocketEvents.createSubscriptionError, creation_errors)
491        self.client.on(WebSocketEvents.sendMessageError, send_errors)
492        self.client.on(WebSocketEvents.subscriptionCreated, created)
493        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
494
495        fake_socket = FlakySendWebSocket(RuntimeError("send failure"))
496        connection_task = await self.start_receive_loop_with(fake_socket)
497
498        with self.assertRaises(Exception):
499            await self.client.create_subscription(events=EVENT_FILTERS)
500        self.assertEqual(len(send_errors.calls), 1)
501        self.assertEqual(len(creation_errors.calls), 1)
502        self.assertEqual(fake_socket.sent, [])
503
504        await self.client.create_subscription(events=OTHER_FILTERS)
505
506        self.assertEqual(len(fake_socket.sent), 1)
507        request = json.loads(fake_socket.sent[0])
508        self.assertEqual(request[1]["eventFilters"], OTHER_FILTERS)
509        fake_socket.push(subscription_creation_frame(request[0]["messageId"]))
510        await self.wait_until(lambda: len(created.calls) == 1)
511
512        fake_socket.push(server_notification_frame())
513        await self.wait_until_frames_delivered(fake_socket)
514        self.assertEqual(len(notifications.calls), 1)
515
516        await self.stop_receive_loop(connection_task)
517        self.assertEqual(len(created.calls), 1)
518
519    async def test_retry_after_rejected_creation_succeeds_without_duplicate_listeners(self):
520        creation_errors = RecordingHandler()
521        created = RecordingHandler()
522        notifications = RecordingHandler()
523        self.client.on(WebSocketEvents.createSubscriptionError, creation_errors)
524        self.client.on(WebSocketEvents.subscriptionCreated, created)
525        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
526
527        fake_socket, connection_task = await self.start_receive_loop()
528        fake_socket.push("[/heartbeat]")
529        await self.wait_until(lambda: self.client._is_ready)
530
531        await self.client.create_subscription(events=EVENT_FILTERS)
532        await self.deliver_creation_response(fake_socket, status=403)
533        await self.wait_until(lambda: len(creation_errors.calls) == 1)
534        self.assertEqual(
535            str(creation_errors.calls[0][0]),
536            "WebSocket subscription creation failed with status 403",
537        )
538        self.assertEqual(created.calls, [])
539
540        await self.client.create_subscription(events=OTHER_FILTERS)
541
542        self.assertEqual(len(fake_socket.sent), 2)
543        retry_request = json.loads(fake_socket.sent[1])
544        self.assertEqual(retry_request[0]["method"], "POST")
545        self.assertEqual(retry_request[1]["eventFilters"], OTHER_FILTERS)
546        fake_socket.push(subscription_creation_frame(retry_request[0]["messageId"]))
547        await self.wait_until(lambda: len(created.calls) == 1)
548
549        fake_socket.push(server_notification_frame())
550        await self.wait_until_frames_delivered(fake_socket)
551        self.assertEqual(len(notifications.calls), 1)
552
553        await self.stop_receive_loop(connection_task)
554        self.assertEqual(len(created.calls), 1)
555        self.assertEqual(len(creation_errors.calls), 1)
556
557    async def test_creation_after_removal_reuses_client_subscription_without_duplicate_listeners(self):
558        created = RecordingHandler()
559        removed = RecordingHandler()
560        notifications = RecordingHandler()
561        self.client.on(WebSocketEvents.subscriptionCreated, created)
562        self.client.on(WebSocketEvents.subscriptionRemoved, removed)
563        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
564
565        fake_socket, connection_task = await self.start_receive_loop()
566        fake_socket.push("[/heartbeat]")
567        await self.wait_until(lambda: self.client._is_ready)
568
569        await self.client.create_subscription(events=EVENT_FILTERS)
570        await self.deliver_creation_response(fake_socket)
571        await self.wait_until(lambda: len(created.calls) == 1)
572        subscription = created.calls[0][0]
573
574        await self.client.remove_subscription(subscription)
575        removal_request = json.loads(fake_socket.sent[1])
576        self.assertEqual(removal_request[0]["method"], "DELETE")
577        fake_socket.push(subscription_removal_frame(removal_request[0]["messageId"]))
578        await self.wait_until(lambda: len(removed.calls) == 1)
579
580        fake_socket.push(server_notification_frame())
581        await self.wait_until_frames_delivered(fake_socket)
582        self.assertEqual(notifications.calls, [])
583
584        await self.client.create_subscription(events=OTHER_FILTERS)
585
586        self.assertEqual(len(fake_socket.sent), 3)
587        recreated_request = json.loads(fake_socket.sent[2])
588        self.assertEqual(recreated_request[0]["method"], "POST")
589        self.assertEqual(recreated_request[1]["eventFilters"], OTHER_FILTERS)
590        fake_socket.push(subscription_creation_frame(recreated_request[0]["messageId"]))
591        await self.wait_until(lambda: len(created.calls) == 2)
592
593        fake_socket.push(server_notification_frame())
594        await self.wait_until_frames_delivered(fake_socket)
595        self.assertEqual(len(notifications.calls), 1)
596
597        await self.stop_receive_loop(connection_task)
598        self.assertEqual(len(created.calls), 2)
599        self.assertEqual(len(removed.calls), 1)
600
601    async def test_successful_creation_returns_none_and_delivers_subscription_via_event(self):
602        created = RecordingHandler()
603        self.client.on(WebSocketEvents.subscriptionCreated, created)
604
605        fake_socket, connection_task = await self.start_receive_loop()
606        fake_socket.push("[/heartbeat]")
607        await self.wait_until(lambda: self.client._is_ready)
608
609        result = await self.client.create_subscription(events=EVENT_FILTERS)
610        self.assertIsNone(result)
611
612        await self.deliver_creation_response(fake_socket)
613        await self.wait_until(lambda: len(created.calls) == 1)
614
615        self.assertEqual(len(created.calls), 1)
616        subscription = created.calls[0][0]
617        self.assertEqual(subscription.get_subscription_info()[1]["id"], "fake-subscription")
618
619        await self.stop_receive_loop(connection_task)
620
621    async def test_recv_failure_after_handshake_reports_receive_error_and_cleans_up(self):
622        receive_error = RuntimeError("receive failure")
623        receive_errors = RecordingHandler()
624        connection_errors = RecordingHandler()
625        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
626        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
627
628        sockets = []
629
630        def on_connected(*_):
631            sockets[0].fail_next_receive(receive_error)
632
633        self.client.on(WebSocketEvents.connectionCreated, on_connected)
634
635        fake_socket, connection_task = await self.start_receive_loop()
636        fake_socket.push("[/heartbeat]")
637        sockets.append(fake_socket)
638
639        result = await connection_task
640
641        self.assertIsNone(result)
642        self.assertEqual(len(receive_errors.calls), 1)
643        self.assertEqual(len(receive_errors.calls[0]), 1)
644        self.assertIs(receive_errors.calls[0][0], receive_error)
645        self.assertEqual(connection_errors.calls, [])
646        self.assertFalse(self.client._is_ready)
647        await self.wait_until(lambda: self.client._heartbeat_task.done())
648        self.assertTrue(self.client._heartbeat_task.cancelled())
649
650    async def test_receive_loop_cancellation_marks_client_not_ready_and_cancels_heartbeat(self):
651        receive_errors = RecordingHandler()
652        connection_errors = RecordingHandler()
653        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
654        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
655
656        fake_socket, connection_task = await self.start_receive_loop()
657        fake_socket.push("[/heartbeat]")
658        await self.wait_until(lambda: self.client._is_ready)
659        self.assertTrue(self.client._is_ready)
660
661        await self.stop_receive_loop(connection_task)
662
663        self.assertFalse(self.client._is_ready)
664        await self.wait_until(lambda: self.client._heartbeat_task.done())
665        self.assertTrue(self.client._heartbeat_task.cancelled())
666        self.assertEqual(receive_errors.calls, [])
667        self.assertEqual(connection_errors.calls, [])
668
669    async def test_intentional_close_performs_cleanup_without_receive_error(self):
670        receive_errors = RecordingHandler()
671        connection_errors = RecordingHandler()
672        close_errors = RecordingHandler()
673        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
674        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
675        self.client.on(WebSocketEvents.closeConnectionError, close_errors)
676
677        fake_socket, connection_task = await self.start_receive_loop()
678        fake_socket.push("[/heartbeat]")
679        await self.wait_until(lambda: self.client._is_ready)
680
681        await self.client.close_connection()
682        result = await connection_task
683
684        self.assertIsNone(result)
685        self.assertTrue(fake_socket.closed)
686        self.assertFalse(self.client._is_ready)
687        await self.wait_until(lambda: self.client._heartbeat_task.done())
688        self.assertTrue(self.client._heartbeat_task.cancelled())
689        self.assertEqual(receive_errors.calls, [])
690        self.assertEqual(connection_errors.calls, [])
691        self.assertEqual(close_errors.calls, [])
692
693    async def test_receive_failure_on_reconnected_client_is_reported_after_intentional_close(self):
694        receive_error = RuntimeError("receive failure after reconnect")
695        receive_errors = RecordingHandler()
696        connection_errors = RecordingHandler()
697        close_errors = RecordingHandler()
698        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
699        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
700        self.client.on(WebSocketEvents.closeConnectionError, close_errors)
701
702        sockets = []
703
704        def fail_latest_socket(*_):
705            sockets[-1].fail_next_receive(receive_error)
706
707        self.client.on(WebSocketEvents.connectionCreated, fail_latest_socket)
708
709        first_socket, connection_task = await self.start_receive_loop()
710        sockets.append(first_socket)
711        first_socket.push("[/heartbeat]")
712        await self.wait_until(lambda: self.client._is_ready)
713
714        await self.client.close_connection()
715        await connection_task
716        self.assertTrue(first_socket.closed)
717
718        second_socket = FakeWebSocket()
719        sockets.append(second_socket)
720
721        async def connect_again(uri):
722            return second_socket
723
724        with mock.patch("websockets.connect", connect_again):
725            recovery_task = asyncio.create_task(self.client.recover_connection())
726            second_socket.push("[/heartbeat]")
727            await self.wait_until(lambda: self.client._is_ready)
728            result = await recovery_task
729
730        self.assertIsNone(result)
731        self.assertEqual(len(receive_errors.calls), 1)
732        self.assertEqual(len(receive_errors.calls[0]), 1)
733        self.assertIs(receive_errors.calls[0][0], receive_error)
734        self.assertEqual(connection_errors.calls, [])
735        self.assertEqual(close_errors.calls, [])
736        self.assertFalse(self.client._is_ready)
737        await self.wait_until(lambda: self.client._heartbeat_task.done())
738        self.assertTrue(self.client._heartbeat_task.cancelled())
739
740    async def test_connection_establishment_failure_retains_create_connection_error(self):
741        connect_error = RuntimeError("connect failure")
742        receive_errors = RecordingHandler()
743        connection_errors = RecordingHandler()
744        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
745        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
746
747        async def connect(uri):
748            raise connect_error
749
750        with mock.patch("websockets.connect", connect):
751            with self.assertRaises(RuntimeError):
752                await self.client.create_new_connection()
753
754        self.assertTrue(connection_errors.calls)
755        for call in connection_errors.calls:
756            self.assertIs(call[0], connect_error)
757        self.assertEqual(receive_errors.calls, [])
758
759    async def test_initial_handshake_failure_retains_create_connection_error(self):
760        handshake_error = RuntimeError("handshake failure")
761        receive_errors = RecordingHandler()
762        connection_errors = RecordingHandler()
763        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
764        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
765
766        fake_socket = FakeWebSocket()
767        fake_socket.fail_next_receive(handshake_error)
768
769        async def connect(uri):
770            return fake_socket
771
772        with mock.patch("websockets.connect", connect):
773            with self.assertRaises(RuntimeError):
774                await self.client.create_new_connection()
775
776        self.assertTrue(connection_errors.calls)
777        for call in connection_errors.calls:
778            self.assertIs(call[0], handshake_error)
779        self.assertEqual(receive_errors.calls, [])
780        self.assertFalse(self.client._is_ready)
781        self.assertIsNone(self.client._heartbeat_task)

A class whose instances are single test cases.

By default, the test code itself should be placed in a method named 'runTest'.

If the fixture may be used for many test cases, create as many test methods as are needed. When instantiating such a TestCase subclass, specify in the constructor arguments the name of the test method that the instance is to execute.

Test authors should subclass TestCase for their own tests. Construction and deconstruction of the test's environment ('fixture') can be implemented by overriding the 'setUp' and 'tearDown' methods respectively.

If it is necessary to override the __init__ method, the base class __init__ method must always be called. It is important that subclasses should not change the signature of their __init__ method, since instances of the classes are instantiated automatically by parts of the framework in order to be run.

When subclassing TestCase, you can set these attributes:

  • failureException: determines which exception will be raised when the instance's assertion methods fail; test methods raising this exception will be deemed to have 'failed' rather than 'errored'.
  • longMessage: determines whether long messages (including repr of objects used in assert methods) will be printed on failure in addition to any explicit message passed.
  • maxDiff: sets the maximum length of a diff in failure messages by assert methods using difflib. It is looked up as an instance attribute so can be configured by individual tests if required.
def setUp(self):
147    def setUp(self):
148        self.client = WebSocketClient(FakePlatform())

Hook method for setting up the test fixture before exercising it.

async def start_receive_loop(self):
150    async def start_receive_loop(self):
151        fake_socket = FakeWebSocket()
152
153        async def connect(uri):
154            return fake_socket
155
156        patcher = mock.patch("websockets.connect", connect)
157        patcher.start()
158        self.addCleanup(patcher.stop)
159        connection_task = asyncio.create_task(self.client.create_new_connection())
160        return fake_socket, connection_task
async def start_receive_loop_with(self, fake_socket):
162    async def start_receive_loop_with(self, fake_socket):
163        async def connect(uri):
164            return fake_socket
165
166        patcher = mock.patch("websockets.connect", connect)
167        patcher.start()
168        self.addCleanup(patcher.stop)
169        connection_task = asyncio.create_task(self.client.create_new_connection())
170        fake_socket.push("[/heartbeat]")
171        await self.wait_until(lambda: self.client._is_ready)
172        return connection_task
async def deliver_creation_response(self, fake_socket, status=200):
174    async def deliver_creation_response(self, fake_socket, status=200):
175        request = json.loads(fake_socket.sent[0])
176        fake_socket.push(subscription_creation_frame(request[0]["messageId"], status=status))
async def wait_until(self, predicate, limit=100):
178    async def wait_until(self, predicate, limit=100):
179        for _ in range(limit):
180            if predicate():
181                return
182            await asyncio.sleep(0)
183        self.fail("condition was not met before the spin limit")
async def wait_until_frames_delivered(self, fake_socket):
185    async def wait_until_frames_delivered(self, fake_socket):
186        await self.wait_until(lambda: not fake_socket.has_pending_frames())
async def stop_receive_loop(self, connection_task):
188    async def stop_receive_loop(self, connection_task):
189        connection_task.cancel()
190        with contextlib.suppress(asyncio.CancelledError):
191            await connection_task
async def test_failing_handler_does_not_stop_delivery_to_remaining_handlers(self):
193    async def test_failing_handler_does_not_stop_delivery_to_remaining_handlers(self):
194        failing = RecordingHandler(error=RuntimeError("handler failure"))
195        healthy = RecordingHandler()
196        receive_errors = RecordingHandler()
197        connection_errors = RecordingHandler()
198        self.client.on(WebSocketEvents.receiveMessage, failing)
199        self.client.on(WebSocketEvents.receiveMessage, healthy)
200        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
201        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
202
203        fake_socket, connection_task = await self.start_receive_loop()
204        fake_socket.push("[/heartbeat]")
205        fake_socket.push("raw-message")
206        await self.wait_until_frames_delivered(fake_socket)
207        await self.stop_receive_loop(connection_task)
208
209        self.assertEqual(failing.calls, [("raw-message",)])
210        self.assertEqual(healthy.calls, [("raw-message",)])
211        self.assertEqual(len(receive_errors.calls), 1)
212        self.assertEqual(len(receive_errors.calls[0]), 1)
213        self.assertIs(receive_errors.calls[0][0], failing.error)
214        self.assertEqual(connection_errors.calls, [])
async def test_failing_handler_without_error_observers_does_not_stop_reception(self):
216    async def test_failing_handler_without_error_observers_does_not_stop_reception(self):
217        failing = RecordingHandler(error=RuntimeError("handler failure"))
218        healthy = RecordingHandler()
219        self.client.on(WebSocketEvents.receiveMessage, failing)
220        self.client.on(WebSocketEvents.receiveMessage, healthy)
221
222        fake_socket, connection_task = await self.start_receive_loop()
223        fake_socket.push("[/heartbeat]")
224        fake_socket.push("raw-message")
225        await self.wait_until_frames_delivered(fake_socket)
226        await self.stop_receive_loop(connection_task)
227
228        self.assertEqual(healthy.calls, [("raw-message",)])
async def test_future_messages_continue_and_failed_handler_remains_registered(self):
230    async def test_future_messages_continue_and_failed_handler_remains_registered(self):
231        failing = RecordingHandler(error=RuntimeError("handler failure"))
232        healthy = RecordingHandler()
233        receive_errors = RecordingHandler()
234        self.client.on(WebSocketEvents.receiveMessage, failing)
235        self.client.on(WebSocketEvents.receiveMessage, healthy)
236        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
237
238        fake_socket, connection_task = await self.start_receive_loop()
239        fake_socket.push("[/heartbeat]")
240        fake_socket.push("first-message")
241        fake_socket.push("second-message")
242        await self.wait_until_frames_delivered(fake_socket)
243        await self.stop_receive_loop(connection_task)
244
245        self.assertEqual(failing.calls, [("first-message",), ("second-message",)])
246        self.assertEqual(healthy.calls, [("first-message",), ("second-message",)])
247        self.assertEqual(
248            [call[0] for call in receive_errors.calls],
249            [failing.error, failing.error],
250        )
async def test_each_failing_handler_produces_its_own_error_event(self):
252    async def test_each_failing_handler_produces_its_own_error_event(self):
253        first = RecordingHandler(error=RuntimeError("first failure"))
254        second = RecordingHandler(error=ValueError("second failure"))
255        healthy = RecordingHandler()
256        receive_errors = RecordingHandler()
257        self.client.on(WebSocketEvents.receiveMessage, first)
258        self.client.on(WebSocketEvents.receiveMessage, healthy)
259        self.client.on(WebSocketEvents.receiveMessage, second)
260        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
261
262        fake_socket, connection_task = await self.start_receive_loop()
263        fake_socket.push("[/heartbeat]")
264        fake_socket.push("raw-message")
265        await self.wait_until_frames_delivered(fake_socket)
266        await self.stop_receive_loop(connection_task)
267
268        self.assertEqual(first.calls, [("raw-message",)])
269        self.assertEqual(second.calls, [("raw-message",)])
270        self.assertEqual(healthy.calls, [("raw-message",)])
271        self.assertEqual(
272            [call[0] for call in receive_errors.calls],
273            [first.error, second.error],
274        )
async def test_subscription_parser_failures_are_isolated_and_reception_continues(self):
276    async def test_subscription_parser_failures_are_isolated_and_reception_continues(self):
277        raw_messages = RecordingHandler()
278        receive_errors = RecordingHandler()
279        notifications = RecordingHandler()
280        subscription_created = RecordingHandler()
281        connection_errors = RecordingHandler()
282        connection_created = RecordingHandler()
283        self.client.on(WebSocketEvents.receiveMessage, raw_messages)
284        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
285        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
286        self.client.on(WebSocketEvents.subscriptionCreated, subscription_created)
287        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
288        self.client.on(WebSocketEvents.connectionCreated, connection_created)
289
290        fake_socket, connection_task = await self.start_receive_loop()
291        fake_socket.push("[/heartbeat]")
292        await self.wait_until(lambda: len(connection_created.calls) == 1)
293
294        await self.client.create_subscription(events=EVENT_FILTERS)
295
296        request = json.loads(fake_socket.sent[0])
297        creation_frame = subscription_creation_frame(request[0]["messageId"])
298        notification_frame = server_notification_frame()
299        fake_socket.push(creation_frame)
300        fake_socket.push("not-json")
301        fake_socket.push("{}")
302        fake_socket.push("[]")
303        fake_socket.push(notification_frame)
304        await self.wait_until_frames_delivered(fake_socket)
305        await self.stop_receive_loop(connection_task)
306
307        self.assertEqual(len(subscription_created.calls), 1)
308        subscription = subscription_created.calls[0][0]
309        self.assertEqual(subscription.get_subscription_info()[1]["id"], "fake-subscription")
310
311        self.assertEqual(raw_messages.calls, [
312            (creation_frame,),
313            ("not-json",),
314            ("{}",),
315            ("[]",),
316            (notification_frame,),
317        ])
318        self.assertEqual(len(receive_errors.calls), 3)
319        self.assertIsInstance(receive_errors.calls[0][0], json.JSONDecodeError)
320        self.assertIsInstance(receive_errors.calls[1][0], KeyError)
321        self.assertIsInstance(receive_errors.calls[2][0], IndexError)
322        self.assertEqual(len(notifications.calls), 1)
323        self.assertEqual(notifications.calls[0][0], json.loads(notification_frame))
324        self.assertEqual(connection_errors.calls, [])
async def test_failing_error_observer_does_not_block_other_error_observers_or_reception(self):
326    async def test_failing_error_observer_does_not_block_other_error_observers_or_reception(self):
327        failing_receive = RecordingHandler(error=RuntimeError("handler failure"))
328        healthy_receive = RecordingHandler()
329        failing_error_observer = RecordingHandler(error=RuntimeError("observer failure"))
330        recording_error_observer = RecordingHandler()
331        self.client.on(WebSocketEvents.receiveMessage, failing_receive)
332        self.client.on(WebSocketEvents.receiveMessage, healthy_receive)
333        self.client.on(WebSocketEvents.receiveMessageError, failing_error_observer)
334        self.client.on(WebSocketEvents.receiveMessageError, recording_error_observer)
335
336        fake_socket, connection_task = await self.start_receive_loop()
337        fake_socket.push("[/heartbeat]")
338        fake_socket.push("first-message")
339        fake_socket.push("second-message")
340        await self.wait_until_frames_delivered(fake_socket)
341        await self.stop_receive_loop(connection_task)
342
343        self.assertEqual(recording_error_observer.calls, [
344            (failing_receive.error,),
345            (failing_receive.error,),
346        ])
347        self.assertEqual(failing_receive.calls, [("first-message",), ("second-message",)])
348        self.assertEqual(healthy_receive.calls, [("first-message",), ("second-message",)])
async def test_unrelated_event_dispatch_semantics_remain_unchanged(self):
350    async def test_unrelated_event_dispatch_semantics_remain_unchanged(self):
351        failing = RecordingHandler(error=RuntimeError("unrelated failure"))
352        healthy = RecordingHandler()
353        self.client.on(WebSocketEvents.subscriptionCreated, failing)
354        self.client.on(WebSocketEvents.subscriptionCreated, healthy)
355
356        with self.assertRaises(RuntimeError):
357            self.client.trigger(WebSocketEvents.subscriptionCreated, self.client)
358
359        self.assertEqual(failing.calls, [(self.client,)])
360        self.assertEqual(healthy.calls, [])
async def test_create_subscription_requires_events_argument(self):
362    async def test_create_subscription_requires_events_argument(self):
363        fake_socket, connection_task = await self.start_receive_loop()
364        fake_socket.push("[/heartbeat]")
365        await self.wait_until(lambda: self.client._is_ready)
366
367        with self.assertRaises(TypeError):
368            await self.client.create_subscription()
369
370        await self.stop_receive_loop(connection_task)
371        self.assertEqual(fake_socket.sent, [])
async def test_create_subscription_rejects_none_and_empty_events(self):
373    async def test_create_subscription_rejects_none_and_empty_events(self):
374        creation_errors = RecordingHandler()
375        created = RecordingHandler()
376        notifications = RecordingHandler()
377        self.client.on(WebSocketEvents.createSubscriptionError, creation_errors)
378        self.client.on(WebSocketEvents.subscriptionCreated, created)
379        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
380
381        fake_socket, connection_task = await self.start_receive_loop()
382        fake_socket.push("[/heartbeat]")
383        await self.wait_until(lambda: self.client._is_ready)
384
385        for invalid_events in (None, []):
386            with self.assertRaises(Exception) as ctx:
387                await self.client.create_subscription(events=invalid_events)
388            self.assertEqual(str(ctx.exception), "Events are undefined")
389
390        fake_socket.push(server_notification_frame())
391        await self.wait_until_frames_delivered(fake_socket)
392
393        self.assertEqual(fake_socket.sent, [])
394        self.assertEqual(created.calls, [])
395        self.assertEqual(len(creation_errors.calls), 2)
396        for call in creation_errors.calls:
397            self.assertEqual(str(call[0]), "Events are undefined")
398        self.assertEqual(notifications.calls, [])
399        await self.stop_receive_loop(connection_task)
async def test_second_creation_while_first_is_pending_is_rejected(self):
401    async def test_second_creation_while_first_is_pending_is_rejected(self):
402        creation_errors = RecordingHandler()
403        created = RecordingHandler()
404        notifications = RecordingHandler()
405        self.client.on(WebSocketEvents.createSubscriptionError, creation_errors)
406        self.client.on(WebSocketEvents.subscriptionCreated, created)
407        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
408
409        fake_socket = GatedSendWebSocket()
410        connection_task = await self.start_receive_loop_with(fake_socket)
411
412        first_task = asyncio.create_task(self.client.create_subscription(events=EVENT_FILTERS))
413        await self.wait_until(lambda: fake_socket.send_started.is_set())
414
415        with self.assertRaises(Exception) as ctx:
416            await self.client.create_subscription(events=OTHER_FILTERS)
417        self.assertEqual(
418            str(ctx.exception),
419            "WebSocket subscription creation is already in progress; "
420            "wait for subscriptionCreated or createSubscriptionError before retrying",
421        )
422        self.assertEqual(len(creation_errors.calls), 1)
423        self.assertEqual(str(creation_errors.calls[0][0]), str(ctx.exception))
424
425        fake_socket.send_gate.set()
426        await first_task
427
428        self.assertEqual(len(fake_socket.sent), 1)
429        request = json.loads(fake_socket.sent[0])
430        self.assertEqual(request[0]["method"], "POST")
431        self.assertEqual(request[1]["eventFilters"], EVENT_FILTERS)
432
433        fake_socket.push(subscription_creation_frame(request[0]["messageId"]))
434        await self.wait_until(lambda: len(created.calls) == 1)
435
436        fake_socket.push(server_notification_frame())
437        await self.wait_until_frames_delivered(fake_socket)
438        self.assertEqual(len(notifications.calls), 1)
439
440        await self.stop_receive_loop(connection_task)
441        self.assertEqual(len(created.calls), 1)
442        self.assertEqual(len(creation_errors.calls), 1)
async def test_second_creation_after_confirmed_creation_is_rejected(self):
444    async def test_second_creation_after_confirmed_creation_is_rejected(self):
445        creation_errors = RecordingHandler()
446        created = RecordingHandler()
447        updated = RecordingHandler()
448        notifications = RecordingHandler()
449        self.client.on(WebSocketEvents.createSubscriptionError, creation_errors)
450        self.client.on(WebSocketEvents.subscriptionCreated, created)
451        self.client.on(WebSocketEvents.subscriptionUpdated, updated)
452        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
453
454        fake_socket, connection_task = await self.start_receive_loop()
455        fake_socket.push("[/heartbeat]")
456        await self.wait_until(lambda: self.client._is_ready)
457
458        await self.client.create_subscription(events=EVENT_FILTERS)
459        await self.deliver_creation_response(fake_socket)
460        await self.wait_until(lambda: len(created.calls) == 1)
461        subscription = created.calls[0][0]
462
463        with self.assertRaises(Exception) as ctx:
464            await self.client.create_subscription(events=OTHER_FILTERS)
465        self.assertEqual(
466            str(ctx.exception),
467            "A WebSocket subscription already exists; use update_subscription() "
468            "to change its events or remove_subscription() before creating another",
469        )
470        self.assertEqual(len(creation_errors.calls), 1)
471        self.assertEqual(str(creation_errors.calls[0][0]), str(ctx.exception))
472
473        self.assertEqual(len(fake_socket.sent), 1)
474        self.assertEqual(json.loads(fake_socket.sent[0])[0]["method"], "POST")
475        self.assertEqual(len(created.calls), 1)
476        self.assertEqual(updated.calls, [])
477        self.assertEqual(subscription.get_subscription_info()[1]["id"], "fake-subscription")
478
479        fake_socket.push(server_notification_frame())
480        await self.wait_until_frames_delivered(fake_socket)
481        self.assertEqual(len(notifications.calls), 1)
482
483        await self.stop_receive_loop(connection_task)
async def test_retry_after_send_failure_uses_new_events_without_duplicate_listeners(self):
485    async def test_retry_after_send_failure_uses_new_events_without_duplicate_listeners(self):
486        creation_errors = RecordingHandler()
487        send_errors = RecordingHandler()
488        created = RecordingHandler()
489        notifications = RecordingHandler()
490        self.client.on(WebSocketEvents.createSubscriptionError, creation_errors)
491        self.client.on(WebSocketEvents.sendMessageError, send_errors)
492        self.client.on(WebSocketEvents.subscriptionCreated, created)
493        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
494
495        fake_socket = FlakySendWebSocket(RuntimeError("send failure"))
496        connection_task = await self.start_receive_loop_with(fake_socket)
497
498        with self.assertRaises(Exception):
499            await self.client.create_subscription(events=EVENT_FILTERS)
500        self.assertEqual(len(send_errors.calls), 1)
501        self.assertEqual(len(creation_errors.calls), 1)
502        self.assertEqual(fake_socket.sent, [])
503
504        await self.client.create_subscription(events=OTHER_FILTERS)
505
506        self.assertEqual(len(fake_socket.sent), 1)
507        request = json.loads(fake_socket.sent[0])
508        self.assertEqual(request[1]["eventFilters"], OTHER_FILTERS)
509        fake_socket.push(subscription_creation_frame(request[0]["messageId"]))
510        await self.wait_until(lambda: len(created.calls) == 1)
511
512        fake_socket.push(server_notification_frame())
513        await self.wait_until_frames_delivered(fake_socket)
514        self.assertEqual(len(notifications.calls), 1)
515
516        await self.stop_receive_loop(connection_task)
517        self.assertEqual(len(created.calls), 1)
async def test_retry_after_rejected_creation_succeeds_without_duplicate_listeners(self):
519    async def test_retry_after_rejected_creation_succeeds_without_duplicate_listeners(self):
520        creation_errors = RecordingHandler()
521        created = RecordingHandler()
522        notifications = RecordingHandler()
523        self.client.on(WebSocketEvents.createSubscriptionError, creation_errors)
524        self.client.on(WebSocketEvents.subscriptionCreated, created)
525        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
526
527        fake_socket, connection_task = await self.start_receive_loop()
528        fake_socket.push("[/heartbeat]")
529        await self.wait_until(lambda: self.client._is_ready)
530
531        await self.client.create_subscription(events=EVENT_FILTERS)
532        await self.deliver_creation_response(fake_socket, status=403)
533        await self.wait_until(lambda: len(creation_errors.calls) == 1)
534        self.assertEqual(
535            str(creation_errors.calls[0][0]),
536            "WebSocket subscription creation failed with status 403",
537        )
538        self.assertEqual(created.calls, [])
539
540        await self.client.create_subscription(events=OTHER_FILTERS)
541
542        self.assertEqual(len(fake_socket.sent), 2)
543        retry_request = json.loads(fake_socket.sent[1])
544        self.assertEqual(retry_request[0]["method"], "POST")
545        self.assertEqual(retry_request[1]["eventFilters"], OTHER_FILTERS)
546        fake_socket.push(subscription_creation_frame(retry_request[0]["messageId"]))
547        await self.wait_until(lambda: len(created.calls) == 1)
548
549        fake_socket.push(server_notification_frame())
550        await self.wait_until_frames_delivered(fake_socket)
551        self.assertEqual(len(notifications.calls), 1)
552
553        await self.stop_receive_loop(connection_task)
554        self.assertEqual(len(created.calls), 1)
555        self.assertEqual(len(creation_errors.calls), 1)
async def test_creation_after_removal_reuses_client_subscription_without_duplicate_listeners(self):
557    async def test_creation_after_removal_reuses_client_subscription_without_duplicate_listeners(self):
558        created = RecordingHandler()
559        removed = RecordingHandler()
560        notifications = RecordingHandler()
561        self.client.on(WebSocketEvents.subscriptionCreated, created)
562        self.client.on(WebSocketEvents.subscriptionRemoved, removed)
563        self.client.on(WebSocketEvents.receiveSubscriptionNotification, notifications)
564
565        fake_socket, connection_task = await self.start_receive_loop()
566        fake_socket.push("[/heartbeat]")
567        await self.wait_until(lambda: self.client._is_ready)
568
569        await self.client.create_subscription(events=EVENT_FILTERS)
570        await self.deliver_creation_response(fake_socket)
571        await self.wait_until(lambda: len(created.calls) == 1)
572        subscription = created.calls[0][0]
573
574        await self.client.remove_subscription(subscription)
575        removal_request = json.loads(fake_socket.sent[1])
576        self.assertEqual(removal_request[0]["method"], "DELETE")
577        fake_socket.push(subscription_removal_frame(removal_request[0]["messageId"]))
578        await self.wait_until(lambda: len(removed.calls) == 1)
579
580        fake_socket.push(server_notification_frame())
581        await self.wait_until_frames_delivered(fake_socket)
582        self.assertEqual(notifications.calls, [])
583
584        await self.client.create_subscription(events=OTHER_FILTERS)
585
586        self.assertEqual(len(fake_socket.sent), 3)
587        recreated_request = json.loads(fake_socket.sent[2])
588        self.assertEqual(recreated_request[0]["method"], "POST")
589        self.assertEqual(recreated_request[1]["eventFilters"], OTHER_FILTERS)
590        fake_socket.push(subscription_creation_frame(recreated_request[0]["messageId"]))
591        await self.wait_until(lambda: len(created.calls) == 2)
592
593        fake_socket.push(server_notification_frame())
594        await self.wait_until_frames_delivered(fake_socket)
595        self.assertEqual(len(notifications.calls), 1)
596
597        await self.stop_receive_loop(connection_task)
598        self.assertEqual(len(created.calls), 2)
599        self.assertEqual(len(removed.calls), 1)
async def test_successful_creation_returns_none_and_delivers_subscription_via_event(self):
601    async def test_successful_creation_returns_none_and_delivers_subscription_via_event(self):
602        created = RecordingHandler()
603        self.client.on(WebSocketEvents.subscriptionCreated, created)
604
605        fake_socket, connection_task = await self.start_receive_loop()
606        fake_socket.push("[/heartbeat]")
607        await self.wait_until(lambda: self.client._is_ready)
608
609        result = await self.client.create_subscription(events=EVENT_FILTERS)
610        self.assertIsNone(result)
611
612        await self.deliver_creation_response(fake_socket)
613        await self.wait_until(lambda: len(created.calls) == 1)
614
615        self.assertEqual(len(created.calls), 1)
616        subscription = created.calls[0][0]
617        self.assertEqual(subscription.get_subscription_info()[1]["id"], "fake-subscription")
618
619        await self.stop_receive_loop(connection_task)
async def test_recv_failure_after_handshake_reports_receive_error_and_cleans_up(self):
621    async def test_recv_failure_after_handshake_reports_receive_error_and_cleans_up(self):
622        receive_error = RuntimeError("receive failure")
623        receive_errors = RecordingHandler()
624        connection_errors = RecordingHandler()
625        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
626        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
627
628        sockets = []
629
630        def on_connected(*_):
631            sockets[0].fail_next_receive(receive_error)
632
633        self.client.on(WebSocketEvents.connectionCreated, on_connected)
634
635        fake_socket, connection_task = await self.start_receive_loop()
636        fake_socket.push("[/heartbeat]")
637        sockets.append(fake_socket)
638
639        result = await connection_task
640
641        self.assertIsNone(result)
642        self.assertEqual(len(receive_errors.calls), 1)
643        self.assertEqual(len(receive_errors.calls[0]), 1)
644        self.assertIs(receive_errors.calls[0][0], receive_error)
645        self.assertEqual(connection_errors.calls, [])
646        self.assertFalse(self.client._is_ready)
647        await self.wait_until(lambda: self.client._heartbeat_task.done())
648        self.assertTrue(self.client._heartbeat_task.cancelled())
async def test_receive_loop_cancellation_marks_client_not_ready_and_cancels_heartbeat(self):
650    async def test_receive_loop_cancellation_marks_client_not_ready_and_cancels_heartbeat(self):
651        receive_errors = RecordingHandler()
652        connection_errors = RecordingHandler()
653        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
654        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
655
656        fake_socket, connection_task = await self.start_receive_loop()
657        fake_socket.push("[/heartbeat]")
658        await self.wait_until(lambda: self.client._is_ready)
659        self.assertTrue(self.client._is_ready)
660
661        await self.stop_receive_loop(connection_task)
662
663        self.assertFalse(self.client._is_ready)
664        await self.wait_until(lambda: self.client._heartbeat_task.done())
665        self.assertTrue(self.client._heartbeat_task.cancelled())
666        self.assertEqual(receive_errors.calls, [])
667        self.assertEqual(connection_errors.calls, [])
async def test_intentional_close_performs_cleanup_without_receive_error(self):
669    async def test_intentional_close_performs_cleanup_without_receive_error(self):
670        receive_errors = RecordingHandler()
671        connection_errors = RecordingHandler()
672        close_errors = RecordingHandler()
673        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
674        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
675        self.client.on(WebSocketEvents.closeConnectionError, close_errors)
676
677        fake_socket, connection_task = await self.start_receive_loop()
678        fake_socket.push("[/heartbeat]")
679        await self.wait_until(lambda: self.client._is_ready)
680
681        await self.client.close_connection()
682        result = await connection_task
683
684        self.assertIsNone(result)
685        self.assertTrue(fake_socket.closed)
686        self.assertFalse(self.client._is_ready)
687        await self.wait_until(lambda: self.client._heartbeat_task.done())
688        self.assertTrue(self.client._heartbeat_task.cancelled())
689        self.assertEqual(receive_errors.calls, [])
690        self.assertEqual(connection_errors.calls, [])
691        self.assertEqual(close_errors.calls, [])
async def test_receive_failure_on_reconnected_client_is_reported_after_intentional_close(self):
693    async def test_receive_failure_on_reconnected_client_is_reported_after_intentional_close(self):
694        receive_error = RuntimeError("receive failure after reconnect")
695        receive_errors = RecordingHandler()
696        connection_errors = RecordingHandler()
697        close_errors = RecordingHandler()
698        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
699        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
700        self.client.on(WebSocketEvents.closeConnectionError, close_errors)
701
702        sockets = []
703
704        def fail_latest_socket(*_):
705            sockets[-1].fail_next_receive(receive_error)
706
707        self.client.on(WebSocketEvents.connectionCreated, fail_latest_socket)
708
709        first_socket, connection_task = await self.start_receive_loop()
710        sockets.append(first_socket)
711        first_socket.push("[/heartbeat]")
712        await self.wait_until(lambda: self.client._is_ready)
713
714        await self.client.close_connection()
715        await connection_task
716        self.assertTrue(first_socket.closed)
717
718        second_socket = FakeWebSocket()
719        sockets.append(second_socket)
720
721        async def connect_again(uri):
722            return second_socket
723
724        with mock.patch("websockets.connect", connect_again):
725            recovery_task = asyncio.create_task(self.client.recover_connection())
726            second_socket.push("[/heartbeat]")
727            await self.wait_until(lambda: self.client._is_ready)
728            result = await recovery_task
729
730        self.assertIsNone(result)
731        self.assertEqual(len(receive_errors.calls), 1)
732        self.assertEqual(len(receive_errors.calls[0]), 1)
733        self.assertIs(receive_errors.calls[0][0], receive_error)
734        self.assertEqual(connection_errors.calls, [])
735        self.assertEqual(close_errors.calls, [])
736        self.assertFalse(self.client._is_ready)
737        await self.wait_until(lambda: self.client._heartbeat_task.done())
738        self.assertTrue(self.client._heartbeat_task.cancelled())
async def test_connection_establishment_failure_retains_create_connection_error(self):
740    async def test_connection_establishment_failure_retains_create_connection_error(self):
741        connect_error = RuntimeError("connect failure")
742        receive_errors = RecordingHandler()
743        connection_errors = RecordingHandler()
744        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
745        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
746
747        async def connect(uri):
748            raise connect_error
749
750        with mock.patch("websockets.connect", connect):
751            with self.assertRaises(RuntimeError):
752                await self.client.create_new_connection()
753
754        self.assertTrue(connection_errors.calls)
755        for call in connection_errors.calls:
756            self.assertIs(call[0], connect_error)
757        self.assertEqual(receive_errors.calls, [])
async def test_initial_handshake_failure_retains_create_connection_error(self):
759    async def test_initial_handshake_failure_retains_create_connection_error(self):
760        handshake_error = RuntimeError("handshake failure")
761        receive_errors = RecordingHandler()
762        connection_errors = RecordingHandler()
763        self.client.on(WebSocketEvents.receiveMessageError, receive_errors)
764        self.client.on(WebSocketEvents.createConnectionError, connection_errors)
765
766        fake_socket = FakeWebSocket()
767        fake_socket.fail_next_receive(handshake_error)
768
769        async def connect(uri):
770            return fake_socket
771
772        with mock.patch("websockets.connect", connect):
773            with self.assertRaises(RuntimeError):
774                await self.client.create_new_connection()
775
776        self.assertTrue(connection_errors.calls)
777        for call in connection_errors.calls:
778            self.assertIs(call[0], handshake_error)
779        self.assertEqual(receive_errors.calls, [])
780        self.assertFalse(self.client._is_ready)
781        self.assertIsNone(self.client._heartbeat_task)
Inherited Members
unittest.async_case.IsolatedAsyncioTestCase
IsolatedAsyncioTestCase
asyncSetUp
asyncTearDown
addAsyncCleanup
enterAsyncContext
run
debug
unittest.case.TestCase
failureException
longMessage
maxDiff
addTypeEqualityFunc
addCleanup
enterContext
addClassCleanup
enterClassContext
tearDown
setUpClass
tearDownClass
countTestCases
defaultTestResult
shortDescription
id
subTest
doCleanups
doClassCleanups
skipTest
fail
assertFalse
assertTrue
assertRaises
assertWarns
assertLogs
assertNoLogs
assertEqual
assertNotEqual
assertAlmostEqual
assertNotAlmostEqual
assertSequenceEqual
assertListEqual
assertTupleEqual
assertSetEqual
assertIn
assertNotIn
assertIs
assertIsNot
assertDictEqual
assertDictContainsSubset
assertCountEqual
assertMultiLineEqual
assertLess
assertLessEqual
assertGreater
assertGreaterEqual
assertIsNone
assertIsNotNone
assertIsInstance
assertNotIsInstance
assertRaisesRegex
assertWarnsRegex
assertRegex
assertNotRegex
failUnlessRaises
failIf
assertRaisesRegexp
assertRegexpMatches
assertNotRegexpMatches
failUnlessEqual
assertEquals
failIfEqual
assertNotEquals
failUnlessAlmostEqual
assertAlmostEquals
failIfAlmostEqual
assertNotAlmostEquals
failUnless
assert_