From 5ee9f526460dd0cc7ad2e7fc41061e04a765f377 Mon Sep 17 00:00:00 2001 From: Allie Crevier Date: Tue, 16 Apr 2019 23:18:27 -0700 Subject: [PATCH 1/7] show solo login dialog --- securedrop_client/gui/main.py | 28 +++++++++++++++------------- securedrop_client/logic.py | 5 +---- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/securedrop_client/gui/main.py b/securedrop_client/gui/main.py index 8282fce3d..93c9991a2 100644 --- a/securedrop_client/gui/main.py +++ b/securedrop_client/gui/main.py @@ -22,7 +22,7 @@ import logging from typing import List -from PyQt5.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QDesktopWidget +from PyQt5.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QDesktopWidget, QApplication from securedrop_client import __version__ from securedrop_client.db import Source @@ -54,8 +54,16 @@ def __init__(self, sdc_home: str): """ super().__init__() self.sdc_home = sdc_home - self.controller = None + def setup(self, controller): + """ + Create references to the controller logic and instantiate the various + views used in the UI. + """ + self.controller = controller # Reference the Client logic instance. + self.show_login() + + def show_main_window(self, username: str) -> None: self.setWindowTitle(_("SecureDrop Client {}").format(__version__)) self.setWindowIcon(load_icon(self.icon)) @@ -91,18 +99,11 @@ def __init__(self, sdc_home: str): self.autosize_window() self.show() - def setup(self, controller): - """ - Create references to the controller logic and instantiate the various - views used in the UI. - """ - self.controller = controller # Reference the Client logic instance. - self.left_pane.setup(self, controller) - self.top_pane.setup(controller) - self.update_activity_status(_('Started SecureDrop Client. Please sign in.'), 20000) + self.left_pane.setup(self, self.controller) + self.top_pane.setup(self.controller) + self.main_view.source_list.setup(self.controller) - self.login_dialog = LoginDialog(self) - self.main_view.setup(self.controller) + self.set_logged_in_as(username) def autosize_window(self): """ @@ -117,6 +118,7 @@ def show_login(self): Show the login form. """ self.login_dialog = LoginDialog(self) + self.login_dialog.move(QApplication.desktop().screen().rect().center() - self.rect().center()) self.login_dialog.setup(self.controller) self.login_dialog.reset() self.login_dialog.exec() diff --git a/securedrop_client/logic.py b/securedrop_client/logic.py index 75406c874..4f73ddba5 100644 --- a/securedrop_client/logic.py +++ b/securedrop_client/logic.py @@ -195,9 +195,6 @@ def setup(self): # If possible, update the UI with available sources. self.update_sources() - # Show the login dialog. - self.gui.show_login() - # Create a timer to check for sync status every 30 seconds. self.sync_timer = QTimer() self.sync_timer.timeout.connect(self.update_sync) @@ -351,7 +348,7 @@ def on_authenticate(self, result): # It worked! Sync with the API and update the UI. self.gui.hide_login() self.sync_api() - self.gui.set_logged_in_as(self.api.username) + self.gui.show_main_window(self.api.username) self.start_message_thread() self.start_reply_thread() From a9cabad54824cb6d9f8be6d2d5598f07cbbc8ce6 Mon Sep 17 00:00:00 2001 From: Allie Crevier Date: Tue, 16 Apr 2019 23:36:41 -0700 Subject: [PATCH 2/7] wait until application window loads before trying to update sources --- securedrop_client/logic.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/securedrop_client/logic.py b/securedrop_client/logic.py index 4f73ddba5..aa5e13bfd 100644 --- a/securedrop_client/logic.py +++ b/securedrop_client/logic.py @@ -192,9 +192,6 @@ def setup(self): # triggered by UI events. self.gui.setup(self) - # If possible, update the UI with available sources. - self.update_sources() - # Create a timer to check for sync status every 30 seconds. self.sync_timer = QTimer() self.sync_timer.timeout.connect(self.update_sync) From 652ea979aa64fbe7f358e6b51c3851c8708e8286 Mon Sep 17 00:00:00 2001 From: Allie Crevier Date: Thu, 18 Apr 2019 17:17:31 -0700 Subject: [PATCH 3/7] add offline mode --- securedrop_client/gui/main.py | 9 ++++++--- securedrop_client/gui/widgets.py | 5 +++++ securedrop_client/logic.py | 9 +++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/securedrop_client/gui/main.py b/securedrop_client/gui/main.py index 93c9991a2..d0c774910 100644 --- a/securedrop_client/gui/main.py +++ b/securedrop_client/gui/main.py @@ -21,6 +21,7 @@ """ import logging from typing import List +import sys from PyQt5.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QDesktopWidget, QApplication @@ -63,7 +64,7 @@ def setup(self, controller): self.controller = controller # Reference the Client logic instance. self.show_login() - def show_main_window(self, username: str) -> None: + def show_main_window(self, username: str=None) -> None: self.setWindowTitle(_("SecureDrop Client {}").format(__version__)) self.setWindowIcon(load_icon(self.icon)) @@ -103,7 +104,8 @@ def show_main_window(self, username: str) -> None: self.top_pane.setup(self.controller) self.main_view.source_list.setup(self.controller) - self.set_logged_in_as(username) + if username: + self.set_logged_in_as(username) def autosize_window(self): """ @@ -118,7 +120,8 @@ def show_login(self): Show the login form. """ self.login_dialog = LoginDialog(self) - self.login_dialog.move(QApplication.desktop().screen().rect().center() - self.rect().center()) + self.login_dialog.move( + QApplication.desktop().screen().rect().center() - self.rect().center()) self.login_dialog.setup(self.controller) self.login_dialog.reset() self.login_dialog.exec() diff --git a/securedrop_client/gui/widgets.py b/securedrop_client/gui/widgets.py index 3ca793088..c54dc951b 100644 --- a/securedrop_client/gui/widgets.py +++ b/securedrop_client/gui/widgets.py @@ -899,6 +899,10 @@ def setup(self, controller): self.submit = QPushButton(_('Sign in')) self.submit.clicked.connect(self.validate) + + self.offline_mode = QPushButton(_('Offline mode')) + self.offline_mode.clicked.connect(self.controller.login_offline_mode) + self.error_label = QLabel('') self.error_label.setObjectName('error_label') # Set css id self.error_label.setStyleSheet(self.CSS) # Set styles @@ -913,6 +917,7 @@ def setup(self, controller): layout.addWidget(self.tfa_label) layout.addWidget(self.tfa_field) layout.addWidget(self.submit) + layout.addWidget(self.offline_mode) layout.addWidget(self.error_label) layout.addStretch() diff --git a/securedrop_client/logic.py b/securedrop_client/logic.py index aa5e13bfd..496197841 100644 --- a/securedrop_client/logic.py +++ b/securedrop_client/logic.py @@ -361,6 +361,15 @@ def on_authenticate(self, result): 'Please verify your credentials and try again.') self.gui.show_login_error(error=error) + def login_offline_mode(self): + # It worked! Sync with the API and update the UI. + self.gui.hide_login() + self.sync_api() + self.gui.show_main_window() + self.start_message_thread() + self.start_reply_thread() + self.is_authenticated = False + def on_login_timeout(self): """ Reset the form and indicate the error. From b927aeba17028ac347f74864d3111eef08aef6f8 Mon Sep 17 00:00:00 2001 From: Allie Crevier Date: Thu, 18 Apr 2019 17:25:54 -0700 Subject: [PATCH 4/7] make it so you can view sources and conversations when offline --- securedrop_client/gui/main.py | 1 - securedrop_client/logic.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/securedrop_client/gui/main.py b/securedrop_client/gui/main.py index d0c774910..150eb37ec 100644 --- a/securedrop_client/gui/main.py +++ b/securedrop_client/gui/main.py @@ -21,7 +21,6 @@ """ import logging from typing import List -import sys from PyQt5.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QDesktopWidget, QApplication diff --git a/securedrop_client/logic.py b/securedrop_client/logic.py index 496197841..d71285b31 100644 --- a/securedrop_client/logic.py +++ b/securedrop_client/logic.py @@ -362,13 +362,13 @@ def on_authenticate(self, result): self.gui.show_login_error(error=error) def login_offline_mode(self): - # It worked! Sync with the API and update the UI. self.gui.hide_login() self.sync_api() self.gui.show_main_window() self.start_message_thread() self.start_reply_thread() self.is_authenticated = False + self.update_sources() def on_login_timeout(self): """ From 5c9d78a48cd1f028de95b1bad0a4ac312d16d1a1 Mon Sep 17 00:00:00 2001 From: Allie Crevier Date: Fri, 19 Apr 2019 09:53:42 -0700 Subject: [PATCH 5/7] test new solo login with offline startup --- securedrop_client/gui/main.py | 67 ++++++++++++++++---------------- securedrop_client/gui/widgets.py | 1 - securedrop_client/logic.py | 3 ++ tests/gui/test_main.py | 59 ++++++++++++++++++++++++---- tests/gui/test_widgets.py | 11 ++++++ tests/test_logic.py | 39 +++++++++++++++---- 6 files changed, 130 insertions(+), 50 deletions(-) diff --git a/securedrop_client/gui/main.py b/securedrop_client/gui/main.py index 150eb37ec..e69cfb4b4 100644 --- a/securedrop_client/gui/main.py +++ b/securedrop_client/gui/main.py @@ -22,7 +22,8 @@ import logging from typing import List -from PyQt5.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QDesktopWidget, QApplication +from PyQt5.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QDesktopWidget, \ + QApplication from securedrop_client import __version__ from securedrop_client.db import Source @@ -54,55 +55,53 @@ def __init__(self, sdc_home: str): """ super().__init__() self.sdc_home = sdc_home + # Cache a dict of source.uuid -> SourceConversationWrapper + # We do this to not create/destroy widgets constantly (because it causes UI "flicker") + self.conversations = {} - def setup(self, controller): - """ - Create references to the controller logic and instantiate the various - views used in the UI. - """ - self.controller = controller # Reference the Client logic instance. - self.show_login() - - def show_main_window(self, username: str=None) -> None: self.setWindowTitle(_("SecureDrop Client {}").format(__version__)) self.setWindowIcon(load_icon(self.icon)) + # Top Pane to display activity and error messages + self.top_pane = TopPane() + + # Main Pane to display everything else + self.main_pane = QWidget() + layout = QHBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + self.main_pane.setLayout(layout) + self.left_pane = LeftPane() + self.main_view = MainView(self.main_pane) + self.main_view.source_list.itemSelectionChanged.connect(self.on_source_changed) + layout.addWidget(self.left_pane, 1) + layout.addWidget(self.main_view, 8) + + # Set the main window's central widget to show Top Pane and Main Pane self.central_widget = QWidget() central_widget_layout = QVBoxLayout() central_widget_layout.setContentsMargins(0, 0, 0, 0) central_widget_layout.setSpacing(0) self.central_widget.setLayout(central_widget_layout) self.setCentralWidget(self.central_widget) - - self.top_pane = TopPane() central_widget_layout.addWidget(self.top_pane) + central_widget_layout.addWidget(self.main_pane) - self.widget = QWidget() - widget_layout = QHBoxLayout() - widget_layout.setContentsMargins(0, 0, 0, 0) - widget_layout.setSpacing(0) - self.widget.setLayout(widget_layout) - - self.left_pane = LeftPane() - self.main_view = MainView(self.widget) - self.main_view.source_list.itemSelectionChanged.connect(self.on_source_changed) - - widget_layout.addWidget(self.left_pane, 1) - widget_layout.addWidget(self.main_view, 8) - - central_widget_layout.addWidget(self.widget) - - # Cache a dict of source.uuid -> SourceConversationWrapper - # We do this to not create/destroy widgets constantly (because it causes UI "flicker") - self.conversations = {} + def setup(self, controller): + """ + Create references to the controller logic and instantiate the various + views used in the UI. + """ + self.controller = controller # Reference the Client logic instance. + self.top_pane.setup(self.controller) + self.left_pane.setup(self, self.controller) + self.main_view.source_list.setup(self.controller) + self.show_login() + def show_main_window(self, username: str = None) -> None: self.autosize_window() self.show() - self.left_pane.setup(self, self.controller) - self.top_pane.setup(self.controller) - self.main_view.source_list.setup(self.controller) - if username: self.set_logged_in_as(username) diff --git a/securedrop_client/gui/widgets.py b/securedrop_client/gui/widgets.py index c54dc951b..82085999f 100644 --- a/securedrop_client/gui/widgets.py +++ b/securedrop_client/gui/widgets.py @@ -899,7 +899,6 @@ def setup(self, controller): self.submit = QPushButton(_('Sign in')) self.submit.clicked.connect(self.validate) - self.offline_mode = QPushButton(_('Offline mode')) self.offline_mode.clicked.connect(self.controller.login_offline_mode) diff --git a/securedrop_client/logic.py b/securedrop_client/logic.py index d71285b31..20dac5085 100644 --- a/securedrop_client/logic.py +++ b/securedrop_client/logic.py @@ -362,6 +362,9 @@ def on_authenticate(self, result): self.gui.show_login_error(error=error) def login_offline_mode(self): + """ + Allow user to view in offline mode without authentication. + """ self.gui.hide_login() self.sync_api() self.gui.show_main_window() diff --git a/tests/gui/test_main.py b/tests/gui/test_main.py index da5ffdc8e..d6d374e97 100644 --- a/tests/gui/test_main.py +++ b/tests/gui/test_main.py @@ -20,7 +20,6 @@ def test_init(mocker): mock_li = mocker.MagicMock(return_value=load_icon('icon.png')) mock_lo = mocker.MagicMock(return_value=QHBoxLayout()) mock_lo().addWidget = mocker.MagicMock() - mocker.patch('securedrop_client.gui.main.load_icon', mock_li) mock_lp = mocker.patch('securedrop_client.gui.main.LeftPane') mock_mv = mocker.patch('securedrop_client.gui.main.MainView') @@ -28,9 +27,12 @@ def test_init(mocker): mocker.patch('securedrop_client.gui.main.QMainWindow') w = Window('mock') + + assert w.conversations == {} + assert w.sdc_home == 'mock' mock_li.assert_called_once_with(w.icon) mock_lp.assert_called_once_with() - mock_mv.assert_called_once_with(w.widget) + mock_mv.assert_called_once_with(w.main_pane) assert mock_lo().addWidget.call_count == 2 @@ -41,8 +43,45 @@ def test_setup(mocker): """ w = Window('mock') mock_controller = mocker.MagicMock() + w.show_login = mocker.MagicMock() + w.top_pane = mocker.MagicMock() + w.left_pane = mocker.MagicMock() + w.main_view = mocker.MagicMock() + w.main_view.source_list = mocker.MagicMock() + w.setup(mock_controller) + assert w.controller == mock_controller + w.top_pane.setup.assert_called_once_with(mock_controller) + w.left_pane.setup.assert_called_once_with(w, mock_controller) + w.main_view.source_list.setup.assert_called_once_with(mock_controller) + w.show_login.assert_called_once_with() + + +def test_show_main_window(mocker): + w = Window('mock') + w.autosize_window = mocker.MagicMock() + w.show = mocker.MagicMock() + w.set_logged_in_as = mocker.MagicMock() + + w.show_main_window(username='test_username') + + w.autosize_window.assert_called_once_with() + w.show.assert_called_once_with() + w.set_logged_in_as.assert_called_once_with('test_username') + + +def test_show_main_window_without_username(mocker): + w = Window('mock') + w.autosize_window = mocker.MagicMock() + w.show = mocker.MagicMock() + w.set_logged_in_as = mocker.MagicMock() + + w.show_main_window() + + w.autosize_window.assert_called_once_with() + w.show.assert_called_once_with() + w.set_logged_in_as.called is False def test_autosize_window(mocker): @@ -66,9 +105,8 @@ def test_show_login(mocker): """ The login dialog is displayed with a clean state. """ - mock_controller = mocker.MagicMock() w = Window('mock') - w.setup(mock_controller) + w.controller = mocker.MagicMock() mock_ld = mocker.patch('securedrop_client.gui.main.LoginDialog') w.show_login() @@ -82,11 +120,13 @@ def test_show_login_error(mocker): """ Ensures that an error message is displayed in the login dialog. """ - mock_controller = mocker.MagicMock() w = Window('mock') - w.setup(mock_controller) + w.show_login = mocker.MagicMock() + w.setup(mocker.MagicMock()) w.login_dialog = mocker.MagicMock() + w.show_login_error('boom') + w.login_dialog.error.assert_called_once_with('boom') @@ -94,12 +134,13 @@ def test_hide_login(mocker): """ Ensure the login dialog is closed and hidden. """ - mock_controller = mocker.MagicMock() w = Window('mock') - w.setup(mock_controller) + w.show_login = mocker.MagicMock() ld = mocker.MagicMock() w.login_dialog = ld + w.hide_login() + ld.accept.assert_called_once_with() assert w.login_dialog is None @@ -198,7 +239,9 @@ def test_set_logged_in_as(mocker): """ w = Window('mock') w.left_pane = mocker.MagicMock() + w.set_logged_in_as('test') + w.left_pane.set_logged_in_as.assert_called_once_with('test') diff --git a/tests/gui/test_widgets.py b/tests/gui/test_widgets.py index 70d8eceb4..a54923928 100644 --- a/tests/gui/test_widgets.py +++ b/tests/gui/test_widgets.py @@ -379,6 +379,17 @@ def test_MainView_init(): assert isinstance(mv.view_holder, QWidget) +def test_MainView_setup(mocker): + mv = MainView(None) + mv.source_list = mocker.MagicMock() + controller = mocker.MagicMock() + + mv.setup(controller) + + assert mv.controller == controller + mv.source_list.setup.assert_called_once_with(controller) + + def test_MainView_show_conversation(mocker): """ Ensure the passed-in widget is added to the layout of the main view holder diff --git a/tests/test_logic.py b/tests/test_logic.py index 2fb2debbe..8323a3e1c 100644 --- a/tests/test_logic.py +++ b/tests/test_logic.py @@ -79,14 +79,12 @@ def test_Client_setup(homedir, config, mocker): Ensure the application is set up with the following default state: Using the `config` fixture to ensure the config is written to disk. """ - mock_gui = mocker.MagicMock() - mock_session = mocker.MagicMock() - cl = Client('http://localhost', mock_gui, mock_session, homedir) + cl = Client('http://localhost', mocker.MagicMock(), mocker.MagicMock(), homedir) cl.update_sources = mocker.MagicMock() + cl.setup() + cl.gui.setup.assert_called_once_with(cl) - cl.update_sources.assert_called_once_with() - cl.gui.show_login.assert_called_once_with() def test_Client_start_message_thread(homedir, config, mocker): @@ -242,6 +240,32 @@ def test_Client_login(homedir, config, mocker): cl.on_login_timeout) +def test_Client_login_offline_mode(homedir, config, mocker): + """ + Ensures user is not authenticated when logging in in offline mode and that the correct windows + are displayed. + """ + cl = Client('http://localhost', mocker.MagicMock(), mocker.MagicMock(), homedir) + cl.call_api = mocker.MagicMock() + cl.gui = mocker.MagicMock() + cl.gui.show_main_window = mocker.MagicMock() + cl.gui.hide_login = mocker.MagicMock() + cl.sync_api = mocker.MagicMock() + cl.start_message_thread = mocker.MagicMock() + cl.start_reply_thread = mocker.MagicMock() + cl.update_sources = mocker.MagicMock() + + cl.login_offline_mode() + + assert cl.call_api.called is False + assert cl.is_authenticated is False + cl.gui.show_main_window.assert_called_once_with() + cl.gui.hide_login.assert_called_once_with() + cl.sync_api.assert_called_once_with() + cl.start_message_thread.assert_called_once_with() + cl.update_sources.assert_called_once_with() + + def test_Client_on_authenticate_failed(homedir, config, mocker): """ If the server responds with a negative to the request to authenticate, make @@ -271,11 +295,12 @@ def test_Client_on_authenticate_ok(homedir, config, mocker): cl.start_message_thread = mocker.MagicMock() cl.start_reply_thread = mocker.MagicMock() cl.api.username = 'test' + cl.on_authenticate(True) + cl.sync_api.assert_called_once_with() cl.start_message_thread.assert_called_once_with() - cl.gui.set_logged_in_as.assert_called_once_with('test') - # Error status bar should be cleared + cl.gui.show_main_window.assert_called_once_with('test') cl.gui.clear_error_status.assert_called_once_with() From 9af0067393254bbab7ca991b460835fcd7944d72 Mon Sep 17 00:00:00 2001 From: Allie Crevier Date: Fri, 19 Apr 2019 14:21:13 -0700 Subject: [PATCH 6/7] remove sync_api call when in offline mode --- securedrop_client/logic.py | 1 - tests/test_logic.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/securedrop_client/logic.py b/securedrop_client/logic.py index 20dac5085..e0d8862e5 100644 --- a/securedrop_client/logic.py +++ b/securedrop_client/logic.py @@ -366,7 +366,6 @@ def login_offline_mode(self): Allow user to view in offline mode without authentication. """ self.gui.hide_login() - self.sync_api() self.gui.show_main_window() self.start_message_thread() self.start_reply_thread() diff --git a/tests/test_logic.py b/tests/test_logic.py index 8323a3e1c..3d8a67bb7 100644 --- a/tests/test_logic.py +++ b/tests/test_logic.py @@ -250,7 +250,6 @@ def test_Client_login_offline_mode(homedir, config, mocker): cl.gui = mocker.MagicMock() cl.gui.show_main_window = mocker.MagicMock() cl.gui.hide_login = mocker.MagicMock() - cl.sync_api = mocker.MagicMock() cl.start_message_thread = mocker.MagicMock() cl.start_reply_thread = mocker.MagicMock() cl.update_sources = mocker.MagicMock() @@ -261,7 +260,6 @@ def test_Client_login_offline_mode(homedir, config, mocker): assert cl.is_authenticated is False cl.gui.show_main_window.assert_called_once_with() cl.gui.hide_login.assert_called_once_with() - cl.sync_api.assert_called_once_with() cl.start_message_thread.assert_called_once_with() cl.update_sources.assert_called_once_with() From cd110551abf3370e42bde460b7e9f0dec375ddf2 Mon Sep 17 00:00:00 2001 From: Allie Crevier Date: Fri, 19 Apr 2019 15:40:09 -0700 Subject: [PATCH 7/7] only exit app when the main window is not visible --- securedrop_client/gui/widgets.py | 16 +++++++++++++--- tests/gui/test_widgets.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/securedrop_client/gui/widgets.py b/securedrop_client/gui/widgets.py index 82085999f..8cb071bed 100644 --- a/securedrop_client/gui/widgets.py +++ b/securedrop_client/gui/widgets.py @@ -19,13 +19,15 @@ import logging import arrow import html +import sys +from typing import List +from uuid import uuid4 + from PyQt5.QtCore import Qt, pyqtSlot, QTimer, QSize from PyQt5.QtGui import QIcon, QPalette, QBrush, QColor, QFont, QLinearGradient from PyQt5.QtWidgets import QListWidget, QLabel, QWidget, QListWidgetItem, QHBoxLayout, \ QPushButton, QVBoxLayout, QLineEdit, QScrollArea, QDialog, QAction, QMenu, QMessageBox, \ QToolButton, QSizePolicy, QTextEdit, QStatusBar, QGraphicsDropShadowEffect -from typing import List -from uuid import uuid4 from securedrop_client.db import Source, Message, File, Reply from securedrop_client.gui import SvgLabel, SvgPushButton @@ -856,7 +858,15 @@ class LoginDialog(QDialog): MIN_JOURNALIST_USERNAME = 3 # Journalist.MIN_USERNAME_LEN on server def __init__(self, parent): - super().__init__(parent) + self.parent = parent + super().__init__(self.parent) + + def closeEvent(self, event): + """ + Only exit the application when the main window is not visible. + """ + if not self.parent.isVisible(): + sys.exit(0) def setup(self, controller): self.controller = controller diff --git a/tests/gui/test_widgets.py b/tests/gui/test_widgets.py index a54923928..767efe08a 100644 --- a/tests/gui/test_widgets.py +++ b/tests/gui/test_widgets.py @@ -2,7 +2,7 @@ Make sure the UI widgets are configured correctly and work as expected. """ from PyQt5.QtWidgets import QWidget, QApplication, QWidgetItem, QSpacerItem, QVBoxLayout, \ - QMessageBox, QLabel + QMessageBox, QLabel, QMainWindow from tests import factory from securedrop_client import db, logic from securedrop_client.gui.widgets import MainView, SourceList, SourceWidget, LoginDialog, \ @@ -790,6 +790,36 @@ def test_LoginDialog_validate_input_ok(mocker): mock_controller.login.assert_called_once_with('foo', 'nicelongpassword', '123456') +def test_LoginDialog_closeEvent_exits(mocker): + """ + If the main window is not visible, then exit the application when the LoginDialog receives a + close event. + """ + mw = QMainWindow() + ld = LoginDialog(mw) + sys_exit_fn = mocker.patch('securedrop_client.gui.widgets.sys.exit') + mw.hide() + + ld.closeEvent(event='mock') + + sys_exit_fn.assert_called_once_with(0) + + +def test_LoginDialog_closeEvent_does_not_exit_when_main_window_is_visible(mocker): + """ + If the main window is visible, then to not exit the application when the LoginDialog receives a + close event. + """ + mw = QMainWindow() + ld = LoginDialog(mw) + sys_exit_fn = mocker.patch('securedrop_client.gui.widgets.sys.exit') + mw.show() + + ld.closeEvent(event='mock') + + assert sys_exit_fn.called is False + + def test_SpeechBubble_init(mocker): """ Check the speech bubble is configured correctly (there's a label containing