diff --git a/addons/bus/controllers/websocket.py b/addons/bus/controllers/websocket.py index 63048c088d5..b47fefab8e7 100644 --- a/addons/bus/controllers/websocket.py +++ b/addons/bus/controllers/websocket.py @@ -3,7 +3,7 @@ import json from werkzeug.exceptions import ServiceUnavailable -from odoo.http import Controller, request, route +from odoo.http import Controller, request, route, SessionExpiredException from odoo.addons.base.models.assetsbundle import AssetsBundle from ..models.bus import channel_with_db from ..websocket import WebsocketConnectionHandler @@ -32,9 +32,14 @@ class WebsocketController(Controller): return request.make_response(data, headers) @route('/websocket/peek_notifications', type='json', auth='public', cors='*') - def peek_notifications(self, channels, last): + def peek_notifications(self, channels, last, is_first_poll=False): if not all(isinstance(c, str) for c in channels): raise ValueError("bus.Bus only string channels are allowed.") + if is_first_poll: + # Used to detect when the current session is expired. + request.session['is_websocket_session'] = True + elif 'is_websocket_session' not in request.session: + raise SessionExpiredException() channels = list(set( channel_with_db(request.db, c) for c in request.env['ir.websocket']._build_bus_channel_list(channels) diff --git a/addons/bus/tests/__init__.py b/addons/bus/tests/__init__.py index bb3b9125cbb..4bab7610650 100644 --- a/addons/bus/tests/__init__.py +++ b/addons/bus/tests/__init__.py @@ -3,4 +3,5 @@ from . import test_assetsbundle from . import test_health from . import test_ir_websocket from . import test_websocket_caryall +from . import test_websocket_controller from . import test_websocket_rate_limiting diff --git a/addons/bus/tests/test_websocket_controller.py b/addons/bus/tests/test_websocket_controller.py new file mode 100644 index 00000000000..2c477cca6fe --- /dev/null +++ b/addons/bus/tests/test_websocket_controller.py @@ -0,0 +1,92 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import json + +from odoo.tests import common + + +class TestWebsocketController(common.HttpCase): + 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'])