From da3122602987c513b725a6e298155ee0c11481bb Mon Sep 17 00:00:00 2001 From: Daniel Martin Date: Fri, 8 Jan 2021 15:01:53 +0000 Subject: [PATCH] Fix http poll routes --- openlp/core/api/poll.py | 28 +++------ openlp/core/api/versions/v2/core.py | 5 -- openlp/core/api/websockets.py | 10 +-- openlp/core/api/websocketspoll.py | 63 +++++++++++++++++++ .../openlp_core/api/test_websockets.py | 35 +++++++++-- .../openlp_core/api/v2/test_core.py | 39 +++++++++--- 6 files changed, 138 insertions(+), 42 deletions(-) create mode 100644 openlp/core/api/websocketspoll.py diff --git a/openlp/core/api/poll.py b/openlp/core/api/poll.py index b244d86bd..7d405fbea 100644 --- a/openlp/core/api/poll.py +++ b/openlp/core/api/poll.py @@ -24,16 +24,19 @@ from openlp.core.common.mixins import RegistryProperties class Poller(RegistryProperties): """ Accessed by the web layer to get status type information from the application + + WARNING: + This class is DEPRECATED, if you need the state of the program, use the registry to access variables. + Used only for the deprecated V1 HTTP API. """ def __init__(self): """ Constructor for the poll builder class. """ super(Poller, self).__init__() - self.previous = {} - def raw_poll(self): - return { + def poll(self): + return {'results': { 'counter': self.live_controller.slide_count if self.live_controller.slide_count else 0, 'service': self.service_manager.service_id, 'slide': self.live_controller.selected_row or 0, @@ -45,21 +48,4 @@ class Poller(RegistryProperties): 'version': 3, 'isSecure': self.settings.value('api/authentication enabled'), 'chordNotation': self.settings.value('songs/chord notation') - } - - def poll(self): - """ - Poll OpenLP to determine current state if it has changed. - """ - current = self.raw_poll() - if self.previous != current: - self.previous = current - return {'results': current} - else: - return None - - def poll_first_time(self): - """ - Poll OpenLP to determine the current state. - """ - return {'results': self.raw_poll()} + }} diff --git a/openlp/core/api/versions/v2/core.py b/openlp/core/api/versions/v2/core.py index 59475b38a..479df2f5f 100644 --- a/openlp/core/api/versions/v2/core.py +++ b/openlp/core/api/versions/v2/core.py @@ -30,11 +30,6 @@ core = Blueprint('core', __name__) log = logging.getLogger(__name__) -@core.route('/poll') -def poll(): - return jsonify(Registry().get('poller').poll()) - - @core.route('/display', methods=['POST']) @login_required def toggle_display(): diff --git a/openlp/core/api/websockets.py b/openlp/core/api/websockets.py index 8a482563c..da23b2029 100644 --- a/openlp/core/api/websockets.py +++ b/openlp/core/api/websockets.py @@ -19,8 +19,8 @@ # along with this program. If not, see . # ########################################################################## """ -The :mod:`http` module contains the API web server. This is a lightweight web server used by remotes to interact -with OpenLP. It uses JSON to communicate with the remotes. +The :mod:`websockets` module contains the websockets server. This is a server used by remotes to listen for stage +changes from within OpenLP. It uses JSON to communicate with the remotes. """ import asyncio import json @@ -32,8 +32,10 @@ from websockets import serve from openlp.core.common.mixins import LogMixin, RegistryProperties from openlp.core.common.registry import Registry from openlp.core.threading import ThreadWorker, run_thread +from openlp.core.api.websocketspoll import WebSocketPoller USERS = set() +poller = WebSocketPoller() log = logging.getLogger(__name__) @@ -52,7 +54,7 @@ async def handle_websocket(websocket, path): """ log.debug('WebSocket handle_websocket connection') await register(websocket) - reply = Registry().get('poller').poll_first_time() + reply = poller.get_state() if reply: json_reply = json.dumps(reply).encode() await websocket.send(json_reply) @@ -93,7 +95,7 @@ async def notify_users(): :return: """ if USERS: # asyncio.wait doesn't accept an empty list - reply = Registry().get('poller').poll() + reply = poller.get_state_if_changed() if reply: json_reply = json.dumps(reply).encode() await asyncio.wait([user.send(json_reply) for user in USERS]) diff --git a/openlp/core/api/websocketspoll.py b/openlp/core/api/websocketspoll.py new file mode 100644 index 000000000..b31bbb6fc --- /dev/null +++ b/openlp/core/api/websocketspoll.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +########################################################################## +# OpenLP - Open Source Lyrics Projection # +# ---------------------------------------------------------------------- # +# Copyright (c) 2008-2020 OpenLP Developers # +# ---------------------------------------------------------------------- # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +########################################################################## +from openlp.core.common.mixins import RegistryProperties + + +class WebSocketPoller(RegistryProperties): + """ + Accessed by web sockets to get status type information from the application + """ + def __init__(self): + """ + Constructor for the web sockets poll builder class. + """ + super(WebSocketPoller, self).__init__() + self._previous = {} + + def get_state(self): + return {'results': { + 'counter': self.live_controller.slide_count if self.live_controller.slide_count else 0, + 'service': self.service_manager.service_id, + 'slide': self.live_controller.selected_row or 0, + 'item': self.live_controller.service_item.unique_identifier if self.live_controller.service_item else '', + 'twelve': self.settings.value('api/twelve hour'), + 'blank': self.live_controller.blank_screen.isChecked(), + 'theme': self.live_controller.theme_screen.isChecked(), + 'display': self.live_controller.desktop_screen.isChecked(), + 'version': 3, + 'isSecure': self.settings.value('api/authentication enabled'), + 'chordNotation': self.settings.value('songs/chord notation') + }} + + def get_state_if_changed(self): + """ + Poll OpenLP to determine current state if it has changed. + + This must only be used by web sockets or else we could miss a state change. + + :return: The current application state or None if unchanged since last call + """ + current = self.get_state() + if self._previous != current: + self._previous = current + return current + else: + return None diff --git a/tests/functional/openlp_core/api/test_websockets.py b/tests/functional/openlp_core/api/test_websockets.py index 46a30294f..86f43c2a3 100644 --- a/tests/functional/openlp_core/api/test_websockets.py +++ b/tests/functional/openlp_core/api/test_websockets.py @@ -24,14 +24,14 @@ Functional tests to test the Http Server Class. import pytest from unittest.mock import MagicMock, patch -from openlp.core.api.poll import Poller +from openlp.core.api.websocketspoll import WebSocketPoller from openlp.core.api.websockets import WebSocketServer from openlp.core.common.registry import Registry @pytest.fixture def poller(settings): - poll = Poller() + poll = WebSocketPoller() yield poll @@ -67,9 +67,9 @@ def test_serverstart_not_required(mocked_run_thread, MockWebSocketWorker, regist assert MockWebSocketWorker.call_count == 0, 'The http thread should not have been called' -def test_poll(poller, settings): +def test_poller_get_state(poller, settings): """ - Test the poll function returns the correct JSON + Test the get_state function returns the correct JSON """ # GIVEN: the system is configured with a set of data mocked_service_manager = MagicMock() @@ -84,7 +84,7 @@ def test_poll(poller, settings): Registry().register('live_controller', mocked_live_controller) Registry().register('service_manager', mocked_service_manager) # WHEN: The poller polls - poll_json = poller.poll() + poll_json = poller.get_state() # THEN: the live json should be generated and match expected results assert poll_json['results']['blank'] is True, 'The blank return value should be True' assert poll_json['results']['theme'] is False, 'The theme return value should be False' @@ -95,3 +95,28 @@ def test_poll(poller, settings): assert poll_json['results']['slide'] == 5, 'The slide return value should be 5' assert poll_json['results']['service'] == 21, 'The version return value should be 21' assert poll_json['results']['item'] == '23-34-45', 'The item return value should match 23-34-45' + + +def test_poller_get_state_if_changed(poller, settings): + """ + Test the get_state_if_changed function returns None if the state has not changed + """ + # GIVEN: the system is configured with a set of data + poller._previous = {} + mocked_service_manager = MagicMock() + mocked_service_manager.service_id = 21 + mocked_live_controller = MagicMock() + mocked_live_controller.selected_row = 5 + mocked_live_controller.service_item = MagicMock() + mocked_live_controller.service_item.unique_identifier = '23-34-45' + mocked_live_controller.blank_screen.isChecked.return_value = True + mocked_live_controller.theme_screen.isChecked.return_value = False + mocked_live_controller.desktop_screen.isChecked.return_value = False + Registry().register('live_controller', mocked_live_controller) + Registry().register('service_manager', mocked_service_manager) + # WHEN: The poller polls twice + poll_json = poller.get_state_if_changed() + poll_json2 = poller.get_state_if_changed() + # THEN: The get_state_if_changed function should return None on the second call because the state has not changed + assert poll_json is not None, 'The first get_state_if_changed function call should have not returned None' + assert poll_json2 is None, 'The second get_state_if_changed function should return None' diff --git a/tests/functional/openlp_core/api/v2/test_core.py b/tests/functional/openlp_core/api/v2/test_core.py index 4e65fbb12..1b85fb839 100644 --- a/tests/functional/openlp_core/api/v2/test_core.py +++ b/tests/functional/openlp_core/api/v2/test_core.py @@ -18,7 +18,10 @@ # You should have received a copy of the GNU General Public License # # along with this program. If not, see . # ########################################################################## +from unittest.mock import MagicMock + from openlp.core.common.registry import Registry +from openlp.core.api.poll import Poller from openlp.core.state import State from openlp.core.lib.plugin import PluginStatus, StringContent @@ -53,13 +56,35 @@ def test_system_information(flask_client, settings): assert not res['login_required'] -def test_poll(flask_client): - class FakePoller: - def poll(self): - return {'foo': 'bar'} - Registry.create().register('poller', FakePoller()) - res = flask_client.get('/api/v2/core/poll').get_json() - assert res['foo'] == 'bar' +def test_poll_backend(settings): + """ + Test the raw poll function returns the correct JSON + """ + # GIVEN: the system is configured with a set of data + poller = Poller() + mocked_service_manager = MagicMock() + mocked_service_manager.service_id = 21 + mocked_live_controller = MagicMock() + mocked_live_controller.selected_row = 5 + mocked_live_controller.service_item = MagicMock() + mocked_live_controller.service_item.unique_identifier = '23-34-45' + mocked_live_controller.blank_screen.isChecked.return_value = True + mocked_live_controller.theme_screen.isChecked.return_value = False + mocked_live_controller.desktop_screen.isChecked.return_value = False + Registry().register('live_controller', mocked_live_controller) + Registry().register('service_manager', mocked_service_manager) + # WHEN: The poller polls + poll_json = poller.poll() + # THEN: the live json should be generated and match expected results + assert poll_json['results']['blank'] is True, 'The blank return value should be True' + assert poll_json['results']['theme'] is False, 'The theme return value should be False' + assert poll_json['results']['display'] is False, 'The display return value should be False' + assert poll_json['results']['isSecure'] is False, 'The isSecure return value should be False' + assert poll_json['results']['twelve'] is True, 'The twelve return value should be True' + assert poll_json['results']['version'] == 3, 'The version return value should be 3' + assert poll_json['results']['slide'] == 5, 'The slide return value should be 5' + assert poll_json['results']['service'] == 21, 'The version return value should be 21' + assert poll_json['results']['item'] == '23-34-45', 'The item return value should match 23-34-45' def test_login_get_is_refused(flask_client):