Initial commit: Core packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:45 +02:00
commit 12c29a983b
9512 changed files with 8379910 additions and 0 deletions

View file

@ -0,0 +1,8 @@
from . import common
from . import test_assetsbundle
from . import test_health
from . import test_ir_websocket
from . import test_notify
from . import test_websocket_caryall
from . import test_websocket_controller
from . import test_websocket_rate_limiting

View file

@ -0,0 +1,124 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import struct
from threading import Event
import unittest
from unittest.mock import patch
try:
import websocket
except ImportError:
websocket = None
import odoo.tools
from odoo.tests import HOST, HttpCase, TEST_CURSOR_COOKIE_NAME
from ..websocket import CloseCode, Websocket, WebsocketConnectionHandler
class WebsocketCase(HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
if websocket is None:
cls._logger.warning("websocket-client module is not installed")
raise unittest.SkipTest("websocket-client module is not installed")
cls._WEBSOCKET_URL = f"ws://{HOST}:{odoo.tools.config['http_port']}/websocket"
websocket_allowed_patch = patch.object(WebsocketConnectionHandler, "websocket_allowed", return_value=True)
cls.startClassPatcher(websocket_allowed_patch)
def setUp(self):
super().setUp()
self._websockets = set()
# Used to ensure websocket connections have been closed
# properly.
self._websocket_events = set()
original_serve_forever = WebsocketConnectionHandler._serve_forever
def _mocked_serve_forever(*args):
websocket_closed_event = Event()
self._websocket_events.add(websocket_closed_event)
original_serve_forever(*args)
websocket_closed_event.set()
self._serve_forever_patch = patch.object(
WebsocketConnectionHandler,
'_serve_forever',
wraps=_mocked_serve_forever
)
self.startPatcher(self._serve_forever_patch)
def tearDown(self):
self._close_websockets()
super().tearDown()
def _close_websockets(self):
"""
Close all the connected websockets and wait for the connection
to terminate.
"""
for ws in self._websockets:
if ws.connected:
ws.close(CloseCode.CLEAN)
self.wait_remaining_websocket_connections()
def websocket_connect(self, *args, **kwargs):
"""
Connect a websocket. If no cookie is given, the connection is
opened with a default session. The created websocket is closed
at the end of the test.
"""
if 'cookie' not in kwargs:
self.session = self.authenticate(None, None)
kwargs['cookie'] = f'session_id={self.session.sid}'
kwargs['cookie'] += f';{TEST_CURSOR_COOKIE_NAME}={self.http_request_key}'
kwargs['timeout'] = 10 # keep a large timeout to avoid aving a websocket request escaping the test
ws = websocket.create_connection(
type(self)._WEBSOCKET_URL, *args, **kwargs
)
ws.ping()
ws.recv_data_frame(control_frame=True) # pong
self._websockets.add(ws)
return ws
def subscribe(self, websocket, channels=None, last=None, wait_for_dispatch=True):
""" Subscribe the websocket to the given channels.
:param websocket: The websocket of the client.
:param channels: The list of channels to subscribe to.
:param last: The last notification id the client received.
:param wait_for_dispatch: Whether to wait for the notification
dispatching trigerred by the subscription.
"""
dispatch_bus_notification_done = Event()
original_dispatch_bus_notifications = Websocket._dispatch_bus_notifications
def _mocked_dispatch_bus_notifications(self, *args):
original_dispatch_bus_notifications(self, *args)
dispatch_bus_notification_done.set()
with patch.object(Websocket, '_dispatch_bus_notifications', _mocked_dispatch_bus_notifications):
sub = {'event_name': 'subscribe', 'data': {
'channels': channels or [],
}}
if last:
sub['data']['last'] = last
websocket.send(json.dumps(sub))
if wait_for_dispatch:
dispatch_bus_notification_done.wait(timeout=5)
def wait_remaining_websocket_connections(self):
""" Wait for the websocket connections to terminate. """
for event in self._websocket_events:
event.wait(5)
def assert_close_with_code(self, websocket, expected_code):
"""
Assert that the websocket is closed with the expected_code.
"""
opcode, payload = websocket.recv_data()
# ensure it's a close frame
self.assertEqual(opcode, 8)
code = struct.unpack('!H', payload[:2])[0]
# ensure the close code is the one we expected
self.assertEqual(code, expected_code)

View file

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo.tests
from odoo.osv import expression
@odoo.tests.tagged('post_install', '-at_install', 'assets_bundle')
class BusWebTests(odoo.tests.HttpCase):
def test_bundle_sends_bus(self):
"""
Tests two things:
- Messages are posted to the bus when assets change
i.e. their hash has been recomputed and differ from the attachment's
- The interface deals with those bus messages by displaying one notification
"""
db_name = self.env.registry.db_name
bundle_xml_ids = ('web.assets_common', 'web.assets_backend')
domain = []
for bundle in bundle_xml_ids:
domain = expression.OR([
domain,
[('name', 'ilike', bundle + '%')]
])
# start from a clean slate
self.env['ir.attachment'].search(domain).unlink()
self.env.registry._clear_cache()
sendones = []
def patched_sendone(self, channel, notificationType, message):
""" Control API and number of messages posted to the bus linked to
bundle_changed events """
if notificationType == 'bundle_changed':
sendones.append((channel, message))
self.patch(type(self.env['bus.bus']), '_sendone', patched_sendone)
self.authenticate('admin', 'admin')
self.url_open('/web')
# One sendone for each asset bundle and for each CSS / JS
self.assertEqual(
len(sendones),
4,
'Received %s' % '\n'.join('%s - %s' % (tmp[0], tmp[1]) for tmp in sendones)
)
for (channel, message) in sendones:
self.assertEqual(channel, 'broadcast')
self.assertEqual(len(message), 1)
self.assertTrue(isinstance(message.get('server_version'), str))

View file

@ -0,0 +1,12 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import HttpCase
class TestBusController(HttpCase):
def test_health(self):
response = self.url_open('/websocket/health')
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload['status'], 'pass')
self.assertFalse(response.cookies.get('session_id'))

View file

@ -0,0 +1,13 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import common
class TestIrWebsocket(common.HttpCase):
def test_only_allow_string_channels_from_frontend(self):
with self.assertRaises(ValueError):
self.env['ir.websocket']._subscribe({
'inactivity_period': 1000,
'last': 0,
'channels': [('odoo', 'mail.channel', 5)],
})

View file

@ -0,0 +1,49 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import BaseCase
from ..models.bus import json_dump, get_notify_payloads, NOTIFY_PAYLOAD_MAX_LENGTH
class NotifyTests(BaseCase):
def test_get_notify_payloads(self):
"""
Asserts that the implementation of `get_notify_payloads`
actually splits correctly large payloads
"""
def check_payloads_size(payloads):
for payload in payloads:
self.assertLess(len(payload.encode()), NOTIFY_PAYLOAD_MAX_LENGTH)
channel = ('dummy_db', 'dummy_model', 12345)
channels = [channel]
self.assertLess(len(json_dump(channels).encode()), NOTIFY_PAYLOAD_MAX_LENGTH)
payloads = get_notify_payloads(channels)
self.assertEqual(len(payloads), 1,
"The payload is less then the threshold, "
"there should be 1 payload only, as it shouldn't be split")
channels = [channel] * 100
self.assertLess(len(json_dump(channels).encode()), NOTIFY_PAYLOAD_MAX_LENGTH)
payloads = get_notify_payloads(channels)
self.assertEqual(len(payloads), 1,
"The payload is less then the threshold, "
"there should be 1 payload only, as it shouldn't be split")
check_payloads_size(payloads)
channels = [channel] * 1000
self.assertGreaterEqual(len(json_dump(channels).encode()), NOTIFY_PAYLOAD_MAX_LENGTH)
payloads = get_notify_payloads(channels)
self.assertGreater(len(payloads), 1,
"Payload was larger than the threshold, it should've been split")
check_payloads_size(payloads)
fat_channel = tuple(item * 1000 for item in channel)
channels = [fat_channel]
self.assertEqual(len(channels), 1, "There should be only 1 channel")
self.assertGreaterEqual(len(json_dump(channels).encode()), NOTIFY_PAYLOAD_MAX_LENGTH)
payloads = get_notify_payloads(channels)
self.assertEqual(len(payloads), 1,
"Payload was larger than the threshold, but shouldn't be split, "
"as it contains only 1 channel")
with self.assertRaises(AssertionError):
check_payloads_size(payloads)

View file

@ -0,0 +1,278 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import gc
import json
import os
from collections import defaultdict
from datetime import timedelta
from freezegun import freeze_time
from threading import Event
from unittest import skipIf
from unittest.mock import patch
try:
from websocket._exceptions import WebSocketBadStatusException
except ImportError:
pass
from odoo.api import Environment
from odoo.tests import common, new_test_user
from .common import WebsocketCase
from ..models.bus import dispatch
from ..websocket import (
CloseCode,
Frame,
Opcode,
TimeoutManager,
TimeoutReason,
Websocket,
_websocket_instances
)
@common.tagged('post_install', '-at_install')
class TestWebsocketCaryall(WebsocketCase):
def test_lifecycle_hooks(self):
events = []
with patch.object(Websocket, '_Websocket__event_callbacks', defaultdict(set)):
@Websocket.onopen
def onopen(env, websocket): # pylint: disable=unused-variable
self.assertIsInstance(env, Environment)
self.assertIsInstance(websocket, Websocket)
events.append('open')
@Websocket.onclose
def onclose(env, websocket): # pylint: disable=unused-variable
self.assertIsInstance(env, Environment)
self.assertIsInstance(websocket, Websocket)
events.append('close')
ws = self.websocket_connect()
ws.close(CloseCode.CLEAN)
self.wait_remaining_websocket_connections()
self.assertEqual(events, ['open', 'close'])
def test_instances_weak_set(self):
gc.collect()
first_ws = self.websocket_connect()
second_ws = self.websocket_connect()
self.assertEqual(len(_websocket_instances), 2)
first_ws.close(CloseCode.CLEAN)
second_ws.close(CloseCode.CLEAN)
self.wait_remaining_websocket_connections()
# serve_forever_patch prevent websocket instances from being
# collected. Stop it now.
self._serve_forever_patch.stop()
gc.collect()
self.assertEqual(len(_websocket_instances), 0)
def test_timeout_manager_no_response_timeout(self):
with freeze_time('2022-08-19') as frozen_time:
timeout_manager = TimeoutManager()
# A PING frame was just sent, if no pong has been received
# within TIMEOUT seconds, the connection should have timed out.
timeout_manager.acknowledge_frame_sent(Frame(Opcode.PING))
self.assertEqual(timeout_manager._awaited_opcode, Opcode.PONG)
frozen_time.tick(delta=timedelta(seconds=TimeoutManager.TIMEOUT / 2))
self.assertFalse(timeout_manager.has_timed_out())
frozen_time.tick(delta=timedelta(seconds=TimeoutManager.TIMEOUT / 2))
self.assertTrue(timeout_manager.has_timed_out())
self.assertEqual(timeout_manager.timeout_reason, TimeoutReason.NO_RESPONSE)
timeout_manager = TimeoutManager()
# A CLOSE frame was just sent, if no close has been received
# within TIMEOUT seconds, the connection should have timed out.
timeout_manager.acknowledge_frame_sent(Frame(Opcode.CLOSE))
self.assertEqual(timeout_manager._awaited_opcode, Opcode.CLOSE)
frozen_time.tick(delta=timedelta(seconds=TimeoutManager.TIMEOUT / 2))
self.assertFalse(timeout_manager.has_timed_out())
frozen_time.tick(delta=timedelta(seconds=TimeoutManager.TIMEOUT / 2))
self.assertTrue(timeout_manager.has_timed_out())
self.assertEqual(timeout_manager.timeout_reason, TimeoutReason.NO_RESPONSE)
def test_timeout_manager_keep_alive_timeout(self):
with freeze_time('2022-08-19') as frozen_time:
timeout_manager = TimeoutManager()
frozen_time.tick(delta=timedelta(seconds=timeout_manager._keep_alive_timeout / 2))
self.assertFalse(timeout_manager.has_timed_out())
frozen_time.tick(delta=timedelta(seconds=timeout_manager._keep_alive_timeout / 2 + 1))
self.assertTrue(timeout_manager.has_timed_out())
self.assertEqual(timeout_manager.timeout_reason, TimeoutReason.KEEP_ALIVE)
def test_timeout_manager_reset_wait_for(self):
timeout_manager = TimeoutManager()
# PING frame
timeout_manager.acknowledge_frame_sent(Frame(Opcode.PING))
self.assertEqual(timeout_manager._awaited_opcode, Opcode.PONG)
timeout_manager.acknowledge_frame_receipt(Frame(Opcode.PONG))
self.assertIsNone(timeout_manager._awaited_opcode)
# CLOSE frame
timeout_manager.acknowledge_frame_sent(Frame(Opcode.CLOSE))
self.assertEqual(timeout_manager._awaited_opcode, Opcode.CLOSE)
timeout_manager.acknowledge_frame_receipt(Frame(Opcode.CLOSE))
self.assertIsNone(timeout_manager._awaited_opcode)
def test_user_login(self):
websocket = self.websocket_connect()
new_test_user(self.env, login='test_user', password='Password!1')
self.authenticate('test_user', 'Password!1')
# The session with whom the websocket connected has been
# deleted. WebSocket should disconnect in order for the
# session to be updated.
websocket.send(json.dumps({'event_name': 'subscribe'}))
self.assert_close_with_code(websocket, CloseCode.SESSION_EXPIRED)
def test_user_logout_incoming_message(self):
new_test_user(self.env, login='test_user', password='Password!1')
user_session = self.authenticate('test_user', 'Password!1')
websocket = self.websocket_connect(cookie=f'session_id={user_session.sid};')
self.url_open('/web/session/logout')
# The session with whom the websocket connected has been
# deleted. WebSocket should disconnect in order for the
# session to be updated.
websocket.send(json.dumps({'event_name': 'subscribe'}))
self.assert_close_with_code(websocket, CloseCode.SESSION_EXPIRED)
@skipIf(os.getenv("ODOO_FAKETIME_TEST_MODE"), 'This test times out when faketime is used')
def test_user_logout_outgoing_message(self):
odoo_ws = None
def patched_subscribe(self, *args):
nonlocal odoo_ws
odoo_ws = self
new_test_user(self.env, login='test_user', password='Password!1')
user_session = self.authenticate('test_user', 'Password!1')
websocket = self.websocket_connect(cookie=f'session_id={user_session.sid};')
with patch.object(Websocket, 'subscribe', patched_subscribe):
self.subscribe(
websocket,
["channel1"],
self.env["bus.bus"].search([], limit=1, order="id DESC").id or 0,
)
self.url_open('/web/session/logout')
# Simulate postgres notify. The session with whom the websocket
# connected has been deleted. WebSocket should be closed without
# receiving the message.
self.env['bus.bus']._sendone('channel1', 'notif type', 'message')
odoo_ws.trigger_notification_dispatching()
self.assert_close_with_code(websocket, CloseCode.SESSION_EXPIRED)
def test_channel_subscription_disconnect(self):
subscribe_done_event = Event()
original_subscribe = dispatch.subscribe
def patched_subscribe(*args):
original_subscribe(*args)
subscribe_done_event.set()
with patch.object(dispatch, 'subscribe', patched_subscribe):
websocket = self.websocket_connect()
websocket.send(json.dumps({
'event_name': 'subscribe',
'data': {'channels': ['my_channel'], 'last': 0}
}))
subscribe_done_event.wait(timeout=5)
# channel is added as expected to the channel to websocket map.
self.assertIn((self.env.registry.db_name, 'my_channel'), dispatch._channels_to_ws)
websocket.close(CloseCode.CLEAN)
self.wait_remaining_websocket_connections()
# channel is removed as expected when removing the last
# websocket that was listening to this channel.
self.assertNotIn((self.env.registry.db_name, 'my_channel'), dispatch._channels_to_ws)
def test_channel_subscription_update(self):
subscribe_done_event = Event()
original_subscribe = dispatch.subscribe
def patched_subscribe(*args):
original_subscribe(*args)
subscribe_done_event.set()
with patch.object(dispatch, 'subscribe', patched_subscribe):
websocket = self.websocket_connect()
websocket.send(json.dumps({
'event_name': 'subscribe',
'data': {'channels': ['my_channel'], 'last': 0}
}))
subscribe_done_event.wait(timeout=5)
subscribe_done_event.clear()
# channel is added as expected to the channel to websocket map.
self.assertIn((self.env.registry.db_name, 'my_channel'), dispatch._channels_to_ws)
websocket.send(json.dumps({
'event_name': 'subscribe',
'data': {'channels': ['my_channel_2'], 'last': 0}
}))
subscribe_done_event.wait(timeout=5)
# channel is removed as expected when updating the subscription.
self.assertNotIn((self.env.registry.db_name, 'my_channel'), dispatch._channels_to_ws)
def test_trigger_notification(self):
original_subscribe = Websocket.subscribe
odoo_ws = None
def patched_subscribe(self, *args):
nonlocal odoo_ws
odoo_ws = self
original_subscribe(self, *args)
with patch.object(Websocket, 'subscribe', patched_subscribe):
websocket = self.websocket_connect()
bus_last_id = self.env['bus.bus'].sudo().search([], limit=1, order='id desc').id or 0
self.env['bus.bus']._sendone('my_channel', 'notif_type', 'message')
self.subscribe(websocket, ["my_channel"], bus_last_id)
notifications = json.loads(websocket.recv())
self.assertEqual(1, len(notifications))
self.assertEqual(notifications[0]['message']['type'], 'notif_type')
self.assertEqual(notifications[0]['message']['payload'], 'message')
self.env['bus.bus']._sendone('my_channel', 'notif_type', 'another_message')
odoo_ws.trigger_notification_dispatching()
notifications = json.loads(websocket.recv())
# First notification has been received, we should only receive
# the second one.
self.assertEqual(1, len(notifications))
self.assertEqual(notifications[0]['message']['type'], 'notif_type')
self.assertEqual(notifications[0]['message']['payload'], 'another_message')
def test_subscribe_higher_last_notification_id(self):
subscribe_done_event = Event()
server_last_notification_id = self.env['bus.bus'].sudo().search([], limit=1, order='id desc').id or 0
client_last_notification_id = server_last_notification_id + 1
def subscribe_side_effect(_, last):
# Last notification id given by the client is higher than
# the one known by the server, should default to 0.
self.assertEqual(last, 0)
subscribe_done_event.set()
with patch.object(Websocket, 'subscribe', side_effect=subscribe_side_effect):
websocket = self.websocket_connect()
websocket.send(json.dumps({
'event_name': 'subscribe',
'data': {'channels': ['my_channel'], 'last': client_last_notification_id}
}))
subscribe_done_event.wait()
def test_subscribe_lower_last_notification_id(self):
subscribe_done_event = Event()
server_last_notification_id = self.env['bus.bus'].sudo().search([], limit=1, order='id desc').id or 0
client_last_notification_id = server_last_notification_id - 1
def subscribe_side_effect(_, last):
self.assertEqual(last, client_last_notification_id)
subscribe_done_event.set()
with patch.object(Websocket, 'subscribe', side_effect=subscribe_side_effect):
websocket = self.websocket_connect()
websocket.send(json.dumps({
'event_name': 'subscribe',
'data': {'channels': ['my_channel'], 'last': client_last_notification_id}
}))
subscribe_done_event.wait()
def test_no_cursor_when_no_callback_for_lifecycle_event(self):
with patch.object(Websocket, '_Websocket__event_callbacks', defaultdict(set)):
with patch('odoo.addons.bus.websocket.acquire_cursor') as mock:
self.websocket_connect()
self.assertFalse(mock.called)

View file

@ -0,0 +1,92 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
class TestWebsocketController(HttpCaseWithUserDemo):
def _make_rpc(self, route, params, headers=None):
data = json.dumps({
'id': 0,
'jsonrpc': '2.0',
'method': 'call',
'params': params,
}).encode()
headers = headers or {}
headers['Content-Type'] = 'application/json'
return self.url_open(route, data, headers=headers)
def test_websocket_peek(self):
response = json.loads(
self._make_rpc('/websocket/peek_notifications', {
'channels': [],
'last': 0,
'is_first_poll': True,
}).content.decode()
)
# Response containing channels/notifications is retrieved and is
# conform to excpectations.
result = response.get('result')
self.assertIsNotNone(result)
channels = result.get('channels')
self.assertIsNotNone(channels)
self.assertIsInstance(channels, list)
notifications = result.get('notifications')
self.assertIsNotNone(notifications)
self.assertIsInstance(notifications, list)
response = json.loads(
self._make_rpc('/websocket/peek_notifications', {
'channels': [],
'last': 0,
'is_first_poll': False,
}).content.decode()
)
# Reponse is received as long as the session is valid.
self.assertIn('result', response)
def test_websocket_peek_session_expired_login(self):
session = self.authenticate(None, None)
# first rpc should be fine
self._make_rpc('/websocket/peek_notifications', {
'channels': [],
'last': 0,
'is_first_poll': True,
})
self.authenticate('admin', 'admin')
# rpc with outdated session should lead to error.
headers = {'Cookie': f'session_id={session.sid};'}
response = json.loads(
self._make_rpc('/websocket/peek_notifications', {
'channels': [],
'last': 0,
'is_first_poll': False,
}, headers=headers).content.decode()
)
error = response.get('error')
self.assertIsNotNone(error, 'Sending a poll with an outdated session should lead to error')
self.assertEqual('odoo.http.SessionExpiredException', error['data']['name'])
def test_websocket_peek_session_expired_logout(self):
session = self.authenticate('demo', 'demo')
# first rpc should be fine
self._make_rpc('/websocket/peek_notifications', {
'channels': [],
'last': 0,
'is_first_poll': True,
})
self.url_open('/web/session/logout')
# rpc with outdated session should lead to error.
headers = {'Cookie': f'session_id={session.sid};'}
response = json.loads(
self._make_rpc('/websocket/peek_notifications', {
'channels': [],
'last': 0,
'is_first_poll': False,
}, headers=headers).content.decode()
)
error = response.get('error')
self.assertIsNotNone(error, 'Sending a poll with an outdated session should lead to error')
self.assertEqual('odoo.http.SessionExpiredException', error['data']['name'])

View file

@ -0,0 +1,85 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import time
try:
from websocket._exceptions import WebSocketProtocolException
from websocket._abnf import VALID_CLOSE_STATUS
except ImportError:
pass
from odoo.tests import common
from .common import WebsocketCase
from ..websocket import CloseCode, Websocket
@common.tagged('post_install', '-at_install')
class TestWebsocketRateLimiting(WebsocketCase):
def test_rate_limiting_base_ok(self):
ws = self.websocket_connect()
# slepeing after initial ping frame
time.sleep(Websocket.RL_DELAY)
for _ in range(Websocket.RL_BURST + 1):
ws.send(json.dumps({'event_name': 'test_rate_limiting'}))
time.sleep(Websocket.RL_DELAY)
def test_rate_limiting_base_ko(self):
def check_base_ko():
for _ in range(Websocket.RL_BURST + 1):
ws.send(json.dumps({'event_name': 'test_rate_limiting'}))
self.assert_close_with_code(ws, CloseCode.TRY_LATER)
ws = self.websocket_connect()
if 1013 not in VALID_CLOSE_STATUS:
# Websocket client's close codes are not up to date. Indeed, the
# 1013 close code results in a protocol exception while it is a
# valid, registered close code ("TRY LATER") :
# https://www.iana.org/assignments/websocket/websocket.xhtml
with self.assertRaises(WebSocketProtocolException) as cm:
check_base_ko()
self.assertEqual(str(cm.exception), 'Invalid close opcode.')
else:
check_base_ko()
def test_rate_limiting_opening_burst(self):
ws = self.websocket_connect()
# slepeing after initial ping frame
time.sleep(Websocket.RL_DELAY)
# burst is allowed
for _ in range(Websocket.RL_BURST // 2):
ws.send(json.dumps({"event_name": "test_rate_limiting"}))
# as long as the rate is respected afterwards
for _ in range(Websocket.RL_BURST):
time.sleep(Websocket.RL_DELAY * 2)
ws.send(json.dumps({"event_name": "test_rate_limiting"}))
def test_rate_limiting_start_ok_end_ko(self):
def check_end_ko():
# those requests are illicit and should not be accepted.
for _ in range(Websocket.RL_BURST * 2):
ws.send(json.dumps({'event_name': 'test_rate_limiting'}))
self.assert_close_with_code(ws, CloseCode.TRY_LATER)
ws = self.websocket_connect()
# first requests are legit and should be accepted
for _ in range(Websocket.RL_BURST + 1):
ws.send(json.dumps({'event_name': 'test_rate_limiting'}))
time.sleep(Websocket.RL_DELAY)
if 1013 not in VALID_CLOSE_STATUS:
# Websocket client's close codes are not up to date. Indeed, the
# 1013 close code results in a protocol exception while it is a
# valid, registered close code ("TRY LATER") :
# https://www.iana.org/assignments/websocket/websocket.xhtml
with self.assertRaises(WebSocketProtocolException) as cm:
check_end_ko()
self.assertEqual(str(cm.exception), 'Invalid close opcode.')
else:
check_end_ko()