def show_login(self): """ Show the login form. """ self.login_dialog = LoginDialog(self) self.login_dialog.setup(self.controller) self.login_dialog.reset() self.login_dialog.exec()
def test_LoginDialog_setup(): """ The LoginView is correctly initialised. """ mock_controller = mock.MagicMock() ld = LoginDialog(None) ld.setup(mock_controller) assert ld.controller == mock_controller assert ld.title.text() == '<h1>Sign in</h1>'
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()
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.tool_bar.setup(self, controller) self.status_bar = QStatusBar(self) self.setStatusBar(self.status_bar) self.set_status('Started SecureDrop Client. Please sign in.', 20000) self.login_dialog = LoginDialog(self) self.main_view.setup(self.controller)
def test_login_ensure_errors_displayed(qtbot, mocker): """ We see an error if incomplete credentials are supplied to the login dialog. """ w = Window() login_dialog = LoginDialog(w) login_dialog.show() assert login_dialog.error_bar.error_status_bar.text() == "" qtbot.keyClicks(login_dialog.username_field, "journalist") qtbot.mouseClick(login_dialog.submit, Qt.LeftButton) expected = "Please enter a username, passphrase and two-factor code." actual = login_dialog.error_bar.error_status_bar.text() assert actual == expected
def show_login(self): """ Show the login form. """ self.login_dialog = LoginDialog(self) # Always display the login dialog centered in the screen. screen_size = QDesktopWidget().screenGeometry() login_dialog_size = self.login_dialog.geometry() x_center = (screen_size.width() - login_dialog_size.width()) / 2 y_center = (screen_size.height() - login_dialog_size.height()) / 2 self.login_dialog.move(x_center, y_center) self.login_dialog.setup(self.controller) self.login_dialog.reset() self.login_dialog.exec()
def test_LoginDialog_error(): """ Any error message passed in is assigned as the text for the error label. """ mock_controller = mock.MagicMock() ld = LoginDialog(None) ld.setup(mock_controller) ld.error_label = mock.MagicMock() ld.error('foo') ld.error_label.setText.assert_called_once_with('foo')
def test_LoginDialog_validate_no_input(): """ If the user doesn't provide input, tell them and give guidance. """ mock_controller = mock.MagicMock() ld = LoginDialog(None) ld.setup(mock_controller) ld.username_field.text = mock.MagicMock(return_value='') ld.password_field.text = mock.MagicMock(return_value='') ld.tfa_field.text = mock.MagicMock(return_value='') ld.setDisabled = mock.MagicMock() ld.error = mock.MagicMock() ld.validate() assert ld.setDisabled.call_count == 2 assert ld.error.call_count == 1
def test_LoginDialog_validate_input_ok(): """ Valid input from the user causes a call to the controller's login method. """ mock_controller = mock.MagicMock() ld = LoginDialog(None) ld.setup(mock_controller) ld.username_field.text = mock.MagicMock(return_value='foo') ld.password_field.text = mock.MagicMock(return_value='bar') ld.tfa_field.text = mock.MagicMock(return_value='123456') ld.setDisabled = mock.MagicMock() ld.error = mock.MagicMock() ld.validate() assert ld.setDisabled.call_count == 1 assert ld.error.call_count == 0 mock_controller.login.assert_called_once_with('foo', 'bar', '123456')
def test_LoginDialog_validate_too_short_password(mocker): """ If the password is too small, we show an informative error message. """ mock_controller = mocker.MagicMock() ld = LoginDialog(None) ld.setup(mock_controller) ld.username_field.text = mocker.MagicMock(return_value='foo') ld.password_field.text = mocker.MagicMock(return_value='bar') ld.tfa_field.text = mocker.MagicMock(return_value='123456') ld.setDisabled = mocker.MagicMock() ld.error = mocker.MagicMock() ld.validate() assert ld.setDisabled.call_count == 2 assert ld.error.call_count == 1 assert mock_controller.login.call_count == 0
def test_LoginDialog_validate_input_non_numeric_2fa(mocker): """ If the user doesn't provide numeric 2fa input, tell them and give guidance. """ mock_controller = mocker.MagicMock() ld = LoginDialog(None) ld.setup(mock_controller) ld.username_field.text = mocker.MagicMock(return_value='foo') ld.password_field.text = mocker.MagicMock(return_value='nicelongpassword') ld.tfa_field.text = mocker.MagicMock(return_value='baz') ld.setDisabled = mocker.MagicMock() ld.error = mocker.MagicMock() ld.validate() assert ld.setDisabled.call_count == 2 assert ld.error.call_count == 1 assert mock_controller.login.call_count == 0
def test_LoginDialog_reset(): """ Ensure the state of the login view is returned to the correct state. """ mock_controller = mock.MagicMock() ld = LoginDialog(None) ld.setup(mock_controller) ld.username_field = mock.MagicMock() ld.password_field = mock.MagicMock() ld.tfa_field = mock.MagicMock() ld.setDisabled = mock.MagicMock() ld.error_label = mock.MagicMock() ld.reset() ld.username_field.setText.assert_called_once_with('') ld.password_field.setText.assert_called_once_with('') ld.tfa_field.setText.assert_called_once_with('') ld.setDisabled.assert_called_once_with(False) ld.error_label.setText.assert_called_once_with('')
class Window(QMainWindow): """ Represents the application's main window that will contain the UI widgets. All interactions with the UI go through the object created by this class. """ icon = "icon.png" def __init__(self) -> None: """ Create the default start state. The window contains a root widget into which is placed: * A status bar widget at the top, containing curent user / status information. * A main-view widget, itself containing a list view for sources and a place for details / message contents / forms. """ super().__init__() load_font("Montserrat") load_font("Source_Sans_Pro") self.setStyleSheet(load_css("sdclient.css")) 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) layout.addWidget(self.left_pane) layout.addWidget(self.main_view) # 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) central_widget_layout.addWidget(self.top_pane) central_widget_layout.addWidget(self.main_pane) def setup(self, controller): """ Create references to the controller logic and instantiate the various views used in the UI. """ self.controller = controller # Reference the Controller logic instance. self.top_pane.setup(self.controller) self.left_pane.setup(self, self.controller) self.main_view.setup(self.controller) self.show_login() def show_main_window(self, db_user: User = None) -> None: """ Show main application window. """ self.autosize_window() self.show() if db_user: self.set_logged_in_as(db_user) def autosize_window(self): """ Ensure the application window takes up 100% of the available screen (i.e. the whole of the virtualised desktop in Qubes dom) """ screen = QDesktopWidget().screenGeometry() self.resize(screen.width(), screen.height()) def show_login(self, error: str = ""): """ Show the login form. """ self.login_dialog = LoginDialog(self) # Always display the login dialog centered in the screen. screen_size = QDesktopWidget().screenGeometry() login_dialog_size = self.login_dialog.geometry() x_center = (screen_size.width() - login_dialog_size.width()) / 2 y_center = (screen_size.height() - login_dialog_size.height()) / 2 self.login_dialog.move(x_center, y_center) self.login_dialog.setup(self.controller) self.login_dialog.reset() if error: self.login_dialog.error(error) self.login_dialog.show() def show_login_error(self, error): """ Display an error in the login dialog. """ if self.login_dialog and error: self.login_dialog.error(error) def hide_login(self): """ Kill the login dialog. """ self.login_dialog.accept() self.login_dialog = None def refresh_current_source_conversation(self): """ Update the current conversation if the source collection has changed. """ self.main_view.on_source_changed() def show_sources(self, sources: List[Source]): """ Update the left hand sources list in the UI with the passed in list of sources. """ self.main_view.show_sources(sources) def show_last_sync(self, updated_on): """ Display a message indicating the time of last sync with the server. """ if updated_on: self.update_activity_status( _("Last Refresh: {}").format(updated_on.humanize())) else: self.update_activity_status(_("Last Refresh: never")) def set_logged_in_as(self, db_user: User): """ Update the UI to show user logged in with username. """ self.left_pane.set_logged_in_as(db_user) self.top_pane.set_logged_in() def logout(self): """ Update the UI to show the user is logged out. """ self.left_pane.set_logged_out() self.top_pane.set_logged_out() def update_activity_status(self, message: str, duration=0): """ Display an activity status message to the user. Optionally, supply a duration (in milliseconds), the default will continuously show the message. """ self.top_pane.update_activity_status(message, duration) def update_error_status(self, message: str, duration=10000) -> None: """ Display an error status message to the user. Optionally, supply a duration (in milliseconds), the default will continuously show the message. """ self.top_pane.update_error_status(message, duration) def clear_error_status(self): """ Clear any message currently in the error status bar. """ self.top_pane.clear_error_status() def clear_clipboard(self): """ Purge any clipboard contents. """ cb = QApplication.clipboard() cb.clear()
class Window(QMainWindow): """ Represents the application's main window that will contain the UI widgets. All interactions with the UI go through the object created by this class. """ icon = 'icon.png' def __init__(self): """ Create the default start state. The window contains a root widget into which is placed: * A status bar widget at the top, containing curent user / status information. * A main-view widget, itself containing a list view for sources and a place for details / message contents / forms. """ super().__init__() self.setWindowTitle(_("SecureDrop Client {}").format(__version__)) self.setWindowIcon(load_icon(self.icon)) self.widget = QWidget() widget_layout = QVBoxLayout() self.widget.setLayout(widget_layout) self.tool_bar = ToolBar(self.widget) self.main_view = MainView(self.widget) self.main_view.source_list.itemSelectionChanged.\ connect(self.on_source_changed) widget_layout.addWidget(self.tool_bar, 1) widget_layout.addWidget(self.main_view, 6) self.setCentralWidget(self.widget) self.show() self.autosize_window() 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.tool_bar.setup(self, controller) self.status_bar = QStatusBar(self) self.setStatusBar(self.status_bar) self.set_status('Started SecureDrop Client. Please sign in.', 20000) self.login_dialog = LoginDialog(self) self.main_view.setup(self.controller) def autosize_window(self): """ Ensure the application window takes up 100% of the available screen (i.e. the whole of the virtualised desktop in Qubes dom) """ screen = QDesktopWidget().screenGeometry() self.resize(screen.width(), screen.height()) def show_login(self): """ Show the login form. """ self.login_dialog = LoginDialog(self) self.login_dialog.setup(self.controller) self.login_dialog.reset() self.login_dialog.exec() def show_login_error(self, error): """ Display an error in the login dialog. """ if self.login_dialog and error: self.login_dialog.error(error) def hide_login(self): """ Kill the login dialog. """ self.login_dialog.accept() self.login_dialog = None def update_error_status(self, error=None): """ Show an error message on the sidebar. """ self.main_view.update_error_status(error) def show_sources(self, sources): """ Update the left hand sources list in the UI with the passed in list of sources. """ self.main_view.source_list.update(sources) def show_sync(self, updated_on): """ Display a message indicating the data-sync state. """ if updated_on: self.main_view.status.setText('Last Sync: ' + updated_on.humanize()) else: self.main_view.status.setText(_('Waiting to Synchronize')) def set_logged_in_as(self, username): """ Update the UI to show user logged in with username. """ self.tool_bar.set_logged_in_as(username) def logout(self): """ Update the UI to show the user is logged out. """ self.tool_bar.set_logged_out() def on_source_changed(self): """ React to when the selected source has changed. """ source_item = self.main_view.source_list.currentItem() source_widget = self.main_view.source_list.itemWidget(source_item) if source_widget: self.show_conversation_for(source_widget.source) def show_conversation_for(self, source): """ TODO: Finish this... """ conversation = ConversationView(self) conversation.add_message('Source name: {}'.format( source.journalist_designation)) conversation.add_message('Hello, hello, is this thing switched on?') conversation.add_reply('Yes, I can hear you loud and clear!') conversation.add_reply('How can I help?') conversation.add_message('I have top secret documents relating to ' 'a massive technical scandal at the heart ' ' of the Freedom of the Press Foundation. ' 'In a shocking turn of events, it appears ' 'they give away all their software for FREE.') conversation.add_message("Hello: I’m a nurse at one of the trauma " "centers in town. We've had many patients in " "the last several months, all with " "similar/mysterious respiratory issues. My " "staff has noticed that most live down-wind " "from the Dole fields West of 696. Some of " "the patients say they have complained to " "local authorities about sewage smells. One " "said she's spotted a truck spraying a " "sludge of some kind, on the fields at " "night. I'm attaching a video from the " "patient who taped the trucks, and a PDF of " "redacted police reports that other patients " "shared. I don’t know if there's much you " "can do, but if there is I would be happy " "to help.") conversation.add_message("I work at the City Water Department, and a " "man named Reggie Esters is one of our board " "directors. I believe Reggie is related to " "Rep Monica Conyers. He's literally never " "here, and the resume on file for him makes " "no sense. I have a hunch he is not in his " "job legitimately, and think you should look " "into this. Also: someone I work with heard " "him on the phone once, talking about his " "'time' at Jackson—that contradicts his " "resume. It really seems fishy.", ['fishy_cv.PDF (234Kb)', ]) conversation.add_reply("THIS IS IT THIS IS THE TAPE EVERYONE'S " "LOOKING FOR!!!", ['filename.pdf (32Kb)', ]) conversation.add_reply("Hello: I read your story on Sally Dale, and " "her lawsuit against the St. Joseph's " "Orphanage. My great-aunt was one of the nuns " "there. She is willing to be interviewed, but " "does not want her name, location, or any " "identity details released. She feels " "horrible. She wants the children who survived " "to find peace. Thanks.") self.main_view.update_view(conversation) def set_status(self, message, duration=5000): """ Display a status message to the user. Optionally, supply a duration (in milliseconds), the default value being a duration of 5 seconds. """ self.status_bar.showMessage(message, duration)
class Window(QMainWindow): """ Represents the application's main window that will contain the UI widgets. All interactions with the UI go through the object created by this class. """ icon = 'icon.png' def __init__(self): """ Create the default start state. The window contains a root widget into which is placed: * A status bar widget at the top, containing curent user / status information. * A main-view widget, itself containing a list view for sources and a place for details / message contents / forms. """ super().__init__() self.setWindowTitle(_("SecureDrop Controller {}").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) 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) central_widget_layout.addWidget(self.top_pane) central_widget_layout.addWidget(self.main_pane) def setup(self, controller): """ Create references to the controller logic and instantiate the various views used in the UI. """ self.controller = controller # Reference the Controller logic instance. self.top_pane.setup(self.controller) self.left_pane.setup(self, self.controller) self.main_view.setup(self.controller) self.show_login() def show_main_window(self, username: str = None) -> None: self.autosize_window() self.show() if username: self.set_logged_in_as(username) def autosize_window(self): """ Ensure the application window takes up 100% of the available screen (i.e. the whole of the virtualised desktop in Qubes dom) """ screen = QDesktopWidget().screenGeometry() self.resize(screen.width(), screen.height()) 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() def show_login_error(self, error): """ Display an error in the login dialog. """ if self.login_dialog and error: self.login_dialog.error(error) def hide_login(self): """ Kill the login dialog. """ self.login_dialog.accept() self.login_dialog = None def show_sources(self, sources: List[Source]): """ Update the left hand sources list in the UI with the passed in list of sources. """ self.main_view.show_sources(sources) def show_sync(self, updated_on): """ Display a message indicating the data-sync state. """ if updated_on: self.update_activity_status( _('Last refresh: {}').format(updated_on.humanize())) else: self.update_activity_status(_('Waiting to refresh...'), 5000) def set_logged_in_as(self, username): """ Update the UI to show user logged in with username. """ self.left_pane.set_logged_in_as(username) self.top_pane.enable_refresh() def logout(self): """ Update the UI to show the user is logged out. """ self.left_pane.set_logged_out() self.top_pane.disable_refresh() def update_activity_status(self, message: str, duration=0): """ Display an activity status message to the user. Optionally, supply a duration (in milliseconds), the default will continuously show the message. """ self.top_pane.update_activity_status(message, duration) def update_error_status(self, message: str, duration=10000): """ Display an error status message to the user. Optionally, supply a duration (in milliseconds), the default will continuously show the message. """ self.top_pane.update_error_status(message, duration) def clear_error_status(self): """ Clear any message currently in the error status bar. """ self.top_pane.clear_error_status()
class Window(QMainWindow): """ Represents the application's main window that will contain the UI widgets. All interactions with the UI go through the object created by this class. """ icon = 'icon.png' def __init__(self): """ Create the default start state. The window contains a root widget into which is placed: * A status bar widget at the top, containing curent user / status information. * A main-view widget, itself containing a list view for sources and a place for details / message contents / forms. """ super().__init__() self.setWindowTitle(_("SecureDrop Client {}").format(__version__)) self.setWindowIcon(load_icon(self.icon)) self.widget = QWidget() widget_layout = QVBoxLayout() self.widget.setLayout(widget_layout) self.tool_bar = ToolBar(self.widget) self.main_view = MainView(self.widget) self.main_view.source_list.itemSelectionChanged.\ connect(self.on_source_changed) widget_layout.addWidget(self.tool_bar, 1) widget_layout.addWidget(self.main_view, 6) self.setCentralWidget(self.widget) self.current_source = None # Tracks which source is shown self.conversations = {} self.show() self.autosize_window() 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.tool_bar.setup(self, controller) self.status_bar = QStatusBar(self) self.setStatusBar(self.status_bar) self.set_status('Started SecureDrop Client. Please sign in.', 20000) self.login_dialog = LoginDialog(self) self.main_view.setup(self.controller) def autosize_window(self): """ Ensure the application window takes up 100% of the available screen (i.e. the whole of the virtualised desktop in Qubes dom) """ screen = QDesktopWidget().screenGeometry() self.resize(screen.width(), screen.height()) def show_login(self): """ Show the login form. """ self.login_dialog = LoginDialog(self) self.login_dialog.setup(self.controller) self.login_dialog.reset() self.login_dialog.exec() def show_login_error(self, error): """ Display an error in the login dialog. """ if self.login_dialog and error: self.login_dialog.error(error) def hide_login(self): """ Kill the login dialog. """ self.login_dialog.accept() self.login_dialog = None def update_error_status(self, error=None): """ Show an error message on the sidebar. """ self.main_view.update_error_status(error) def show_sources(self, sources): """ Update the left hand sources list in the UI with the passed in list of sources. """ self.main_view.source_list.update(sources) def show_sync(self, updated_on): """ Display a message indicating the data-sync state. """ if updated_on: self.main_view.status.setText('Last refresh: ' + updated_on.humanize()) else: self.main_view.status.setText(_('Waiting to refresh...')) def set_logged_in_as(self, username): """ Update the UI to show user logged in with username. """ self.tool_bar.set_logged_in_as(username) def logout(self): """ Update the UI to show the user is logged out. """ self.tool_bar.set_logged_out() def on_source_changed(self): """ React to when the selected source has changed. """ source_item = self.main_view.source_list.currentItem() source_widget = self.main_view.source_list.itemWidget(source_item) if source_widget: self.current_source = source_widget.source self.show_conversation_for(self.current_source) def add_item_content_or(self, adder, item, default): """ Private helper function to add correct message to conversation widgets """ if item.is_downloaded is False: adder(default) else: adder(item.content) def show_conversation_for(self, source): """ Show conversation of messages and replies between a source and journalists. """ conversation = ConversationView(self) conversation.setup(self.controller) conversation.add_message('Source name: {}'.format( source.journalist_designation)) # Display each conversation item in the source collection. for conversation_item in source.collection: if conversation_item.filename.endswith('msg.gpg'): self.add_item_content_or(conversation.add_message, conversation_item, "<Message not yet downloaded>") elif conversation_item.filename.endswith('reply.gpg'): self.add_item_content_or(conversation.add_reply, conversation_item, "<Reply not yet downloaded>") else: conversation.add_file(source, conversation_item) self.main_view.update_view(conversation) def set_status(self, message, duration=5000): """ Display a status message to the user. Optionally, supply a duration (in milliseconds), the default value being a duration of 5 seconds. """ self.status_bar.showMessage(message, duration)