def main_window_no_key(mocker, homedir): # Setup app = QApplication([]) gui = Window() app.setActiveWindow(gui) gui.show() controller = Controller("http://localhost", gui, mocker.MagicMock(), homedir, proxy=False) controller.qubes = False gui.setup(controller) # Create a source widget source_list = gui.main_view.source_list source = factory.Source(public_key=None) source_list.update([source]) # Create a file widget, message widget, and reply widget mocker.patch("securedrop_client.gui.widgets.humanize_filesize", return_value="100") mocker.patch( "securedrop_client.gui.SecureQLabel.get_elided_text", return_value="1-yellow-doc.gz.gpg" ) source.collection.append( [ factory.File(source=source, filename="1-yellow-doc.gz.gpg"), factory.Message(source=source, filename="2-yellow-msg.gpg"), factory.Reply(source=source, filename="3-yellow-reply.gpg"), ] ) source_list.setCurrentItem(source_list.item(0)) gui.main_view.on_source_changed() yield gui # Teardown gui.login_dialog.close() app.exit()
def test_Controller_completed_api_call_with_current_object( homedir, config, mocker): """ Ensure that cleanup is performed if an API call returns in the expected time. Using the `config` fixture to ensure the config is written to disk. """ mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) mock_thread = mocker.MagicMock() mock_runner = mocker.MagicMock() mock_runner.result = 'result' mock_runner.current_object = 'current_object' co.api_threads = { 'thread_uuid': { 'thread': mock_thread, 'runner': mock_runner, } } mock_user_callback = mocker.MagicMock() mock_arg_spec = mocker.MagicMock(args=['foo', 'current_object']) mocker.patch('securedrop_client.logic.inspect.getfullargspec', return_value=mock_arg_spec) co.completed_api_call('thread_uuid', mock_user_callback) mock_user_callback.assert_called_once_with('result', current_object='current_object')
def test_Controller_is_authenticated_property(homedir, mocker): ''' Check that the @property `is_authenticated`: - Cannot be deleted - Emits the correct signals when updated - Sets internal state to ensure signals are only set when the state changes ''' mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) mock_signal = mocker.patch.object(co, 'authentication_state') # default state is unauthenticated assert co.is_authenticated is False # the property cannot be deleted with pytest.raises(AttributeError): del co.is_authenticated # setting the signal to its current value does not fire the signal co.is_authenticated = False assert not mock_signal.emit.called assert co.is_authenticated is False # setting the property to True sends a signal co.is_authenticated = True mock_signal.emit.assert_called_once_with(True) assert co.is_authenticated is True mock_signal.reset_mock() co.is_authenticated = False mock_signal.emit.assert_called_once_with(False) assert co.is_authenticated is False
def test_Controller_on_reply_success(homedir, mocker): ''' Check that when the result is a success, the client emits the correct signal. ''' mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() mock_reply_init = mocker.patch('securedrop_client.logic.db.Reply') co = Controller('http://localhost', mock_gui, mock_session, homedir) co.api = mocker.Mock() journalist_uuid = 'abc123' co.api.token_journalist_uuid = journalist_uuid mock_reply_succeeded = mocker.patch.object(co, 'reply_succeeded') mock_reply_failed = mocker.patch.object(co, 'reply_failed') reply = sdlocalobjects.Reply(uuid='xyz456', filename='1-wat.gpg') source_uuid = 'foo111' msg_uuid = 'bar222' current_object = (source_uuid, msg_uuid) co.on_reply_success(reply, current_object) co.session.commit.assert_called_once_with() mock_reply_succeeded.emit.assert_called_once_with(msg_uuid) assert not mock_reply_failed.emit.called assert mock_reply_init.called # to prevent stale mocks
def test_Controller_call_api(homedir, config, mocker): """ A new thread and APICallRunner is created / setup. Using the `config` fixture to ensure the config is written to disk. """ mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) co.finish_api_call = mocker.MagicMock() mocker.patch('securedrop_client.logic.QThread') mocker.patch('securedrop_client.logic.APICallRunner') mocker.patch('securedrop_client.logic.QTimer') mock_api_call = mocker.MagicMock() mock_success_callback = mocker.MagicMock() mock_failure_callback = mocker.MagicMock() co.call_api(mock_api_call, mock_success_callback, mock_failure_callback, 'foo', bar='baz') assert len(co.api_threads) == 1 thread_info = co.api_threads[list(co.api_threads.keys())[0]] thread = thread_info['thread'] runner = thread_info['runner'] thread.started.connect.assert_called_once_with(runner.call_api) thread.start.assert_called_once_with() runner.moveToThread.assert_called_once_with(thread) runner.call_succeeded.connect.call_count == 1 runner.call_failed.connect.call_count == 1 runner.call_timed_out.connect.call_count == 1
def test_Controller_authenticated_no_api(homedir, config, mocker): """ If the API is authenticated return True. Using the `config` fixture to ensure the config is written to disk. """ mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) co.api = None assert co.authenticated() is False
def test_Controller_last_sync_no_file(homedir, config, mocker): """ If there's no sync file, then just return None. Using the `config` fixture to ensure the config is written to disk. """ mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) mocker.patch("builtins.open", mocker.MagicMock(side_effect=Exception())) assert co.last_sync() is None
def test_Controller_api_call_timeout(homedir, config, mocker): ''' Using the `config` fixture to ensure the config is written to disk. ''' mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) co.on_api_timeout() mock_gui.update_error_status.assert_called_once_with( 'The connection to the SecureDrop server timed out. Please try again.')
def test_Controller_on_file_downloaded_api_failure(homedir, config, mocker): ''' Using the `config` fixture to ensure the config is written to disk. ''' mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) co.file_ready = mocker.MagicMock() # signal when file is downloaded co.update_sources = mocker.MagicMock() co.api_runner = mocker.MagicMock() test_filename = "1-my-file-location-msg.gpg" co.api_runner.result = ("", test_filename) co.call_reset = mocker.MagicMock() co.set_status = mocker.MagicMock() result_data = Exception('error message') submission_db_object = mocker.MagicMock() submission_db_object.uuid = 'myuuid' submission_db_object.filename = 'filename' co.on_file_download_failure(result_data, current_object=submission_db_object) co.set_status.assert_called_once_with( "The file download failed. Please try again.") co.file_ready.emit.assert_not_called()
def test_Controller_set_activity_status(homedir, config, mocker): """ Ensure the GUI set_status API is called. Using the `config` fixture to ensure the config is written to disk. """ mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) co.set_status("Hello, World!", 1000) mock_gui.update_activity_status.assert_called_once_with( "Hello, World!", 1000)
def test_Controller_on_delete_source_failure(homedir, config, mocker): ''' Using the `config` fixture to ensure the config is written to disk. ''' mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) co.sync_api = mocker.MagicMock() co.on_delete_source_failure(Exception()) co.gui.update_error_status.assert_called_with( 'Failed to delete source at server')
def test_Controller_on_delete_source_success(homedir, config, mocker): ''' Using the `config` fixture to ensure the config is written to disk. ''' mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) co.sync_api = mocker.MagicMock() co.on_delete_source_success(True) co.sync_api.assert_called_with() co.gui.clear_error_status.assert_called_with()
def test_Controller_on_action_requiring_login(homedir, config, mocker): """ Ensure that when on_action_requiring_login is called, an error is shown in the GUI status area. Using the `config` fixture to ensure the config is written to disk. """ mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) co.on_action_requiring_login() mock_gui.update_error_status.assert_called_once_with( 'You must sign in to perform this action.')
def test_Controller_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. """ co = Controller('http://localhost', mocker.MagicMock(), mocker.MagicMock(), homedir) co.update_sources = mocker.MagicMock() co.setup() co.gui.setup.assert_called_once_with(co)
def test_Controller_on_authenticate_failure(homedir, config, mocker): """ If the server responds with a negative to the request to authenticate, make sure the user knows. Using the `config` fixture to ensure the config is written to disk. """ mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) result_data = Exception('oh no') co.on_authenticate_failure(result_data) mock_gui.show_login_error.\ assert_called_once_with(error='There was a problem signing in. Please ' 'verify your credentials and try again.')
def test_Controller_on_file_downloaded_success(homedir, config, mocker): ''' Using the `config` fixture to ensure the config is written to disk. ''' mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) co.update_sources = mocker.MagicMock() co.api_runner = mocker.MagicMock() co.file_ready = mocker.MagicMock() # signal when file is downloaded test_filename = "1-my-file-location-msg.gpg" test_object_uuid = 'uuid-of-downloaded-object' co.call_reset = mocker.MagicMock() result_data = ('this-is-a-sha256-sum', test_filename) submission_db_object = mocker.MagicMock() submission_db_object.uuid = test_object_uuid submission_db_object.filename = test_filename mock_storage = mocker.patch('securedrop_client.logic.storage') mock_gpg = mocker.patch.object(co.gpg, 'decrypt_submission_or_reply', return_value='filepath') mocker.patch('shutil.move') co.on_file_download_success(result_data, current_object=submission_db_object) mock_gpg.call_count == 1 mock_storage.mark_file_as_downloaded.assert_called_once_with( test_object_uuid, mock_session) mock_storage.set_object_decryption_status_with_content.assert_called_once_with( submission_db_object, mock_session, True) # Signal should be emitted with UUID of the successfully downloaded object co.file_ready.emit.assert_called_once_with(test_object_uuid)
def test_Controller_unstars_a_source_if_unstarred(homedir, config, mocker): """ Ensure that the client stars a source if it is unstarred. Using the `config` fixture to ensure the config is written to disk. """ mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) source_db_object = mocker.MagicMock() source_db_object.uuid = mocker.MagicMock() source_db_object.is_starred = False co.call_api = mocker.MagicMock() co.api = mocker.MagicMock() co.api.add_star = mocker.MagicMock() co.on_update_star_success = mocker.MagicMock() co.on_update_star_failure = mocker.MagicMock() source_sdk_object = mocker.MagicMock() mock_source = mocker.patch('sdclientapi.Source') mock_source.return_value = source_sdk_object co.update_star(source_db_object) co.call_api.assert_called_once_with( co.api.add_star, co.on_update_star_success, co.on_update_star_failure, source_sdk_object, ) mock_gui.clear_error_status.assert_called_once_with()
def test_Controller_on_sync_failure(homedir, config, mocker): """ If there's no result to syncing, then don't attempt to update local storage and perhaps implement some as-yet-undefined UI update. Using the `config` fixture to ensure the config is written to disk. """ mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) co.update_sources = mocker.MagicMock() result_data = Exception('Boom') # Not the expected tuple. mock_storage = mocker.patch('securedrop_client.logic.storage') co.on_sync_failure(result_data) assert mock_storage.update_local_storage.call_count == 0 co.update_sources.assert_called_once_with()
def test_Controller_start_reply_thread(homedir, config, mocker): """ When starting reply-fetching thread, make sure we do a few things. Using the `config` fixture to ensure the config is written to disk. """ mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) mock_qthread = mocker.patch('securedrop_client.logic.QThread') mocker.patch('securedrop_client.logic.ReplySync') co.reply_sync = mocker.MagicMock() co.start_reply_thread() co.reply_sync.moveToThread.assert_called_once_with(mock_qthread()) co.reply_thread.started.connect.assert_called_once_with(co.reply_sync.run) co.reply_thread.start.assert_called_once_with()
def test_Controller_login(homedir, config, mocker): """ Ensures the API is called in the expected manner for logging in the user given the username, password and 2fa token. Using the `config` fixture to ensure the config is written to disk. """ mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) co.call_api = mocker.MagicMock() mock_api = mocker.patch('securedrop_client.logic.sdclientapi.API') co.login('username', 'password', '123456') co.call_api.assert_called_once_with(mock_api().authenticate, co.on_authenticate_success, co.on_authenticate_failure)
def test_Controller_last_sync_with_file(homedir, config, mocker): """ The flag indicating the time of the last sync with the API is stored in a dotfile in the user's home directory. If such a file exists, ensure an "arrow" object (representing the date/time) is returned. Using the `config` fixture to ensure the config is written to disk. """ mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) timestamp = '2018-10-10 18:17:13+01:00' mocker.patch("builtins.open", mocker.mock_open(read_data=timestamp)) result = co.last_sync() assert isinstance(result, arrow.Arrow) assert result.format() == timestamp
def modal_dialog(mocker, homedir): app = QApplication([]) gui = Window() gui.show() controller = Controller("http://localhost", gui, mocker.MagicMock(), homedir, proxy=False) controller.qubes = False gui.setup(controller) gui.login_dialog.close() app.setActiveWindow(gui) dialog = ModalDialog() yield dialog dialog.close() app.exit()
def start_app(args, qt_args) -> None: """ Create all the top-level assets for the application, set things up and run the application. Specific tasks include: - set up locale and language. - set up logging. - create an application object. - create a window for the app. - create an API connection to the SecureDrop proxy. - create a SqlAlchemy session to local storage. - configure the client (logic) object. - ensure the application is setup in the default safe starting state. """ configure_locale_and_language() init(args.sdc_home) configure_logging(args.sdc_home) logging.info('Starting SecureDrop Client {}'.format(__version__)) app = QApplication(qt_args) app.setApplicationName('SecureDrop Client') app.setDesktopFileName('org.freedomofthepress.securedrop.client') app.setApplicationVersion(__version__) app.setAttribute(Qt.AA_UseHighDpiPixmaps) load_font('Montserrat') load_font('Source_Sans_Pro') prevent_second_instance(app, args.sdc_home) session_maker = make_session_maker(args.sdc_home) gui = Window() app.setWindowIcon(load_icon(gui.icon)) app.setStyleSheet(load_css('sdclient.css')) controller = Controller("http://localhost:8081/", gui, session_maker, args.sdc_home, not args.no_proxy, not args.no_qubes) controller.setup() configure_signal_handlers(app) timer = QTimer() timer.start(500) timer.timeout.connect(lambda: None) sys.exit(app.exec_())
def export_dialog(mocker, homedir): app = QApplication([]) gui = Window() gui.show() controller = Controller("http://localhost", gui, mocker.MagicMock(), homedir, proxy=False) controller.qubes = False gui.setup(controller) gui.login_dialog.close() app.setActiveWindow(gui) dialog = ExportDialog(controller, "file_uuid", "file_name") dialog.show() yield dialog dialog.close() gui.close()
def test_Controller_on_sync_success_with_key_import_fail( homedir, config, mocker): """ If there's a result to syncing, then update local storage. This is it to check that we can gracefully handle an import failure. Using the `config` fixture to ensure the config is written to disk. """ mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) co.update_sources = mocker.MagicMock() co.api_runner = mocker.MagicMock() mock_source = mocker.MagicMock() mock_source.key = { 'type': 'PGP', 'public': PUB_KEY, } mock_sources = [mock_source] result_data = (mock_sources, 'submissions', 'replies') co.call_reset = mocker.MagicMock() mock_storage = mocker.patch('securedrop_client.logic.storage') mocker.patch.object(co.gpg, 'import_key', side_effect=CryptoError) co.on_sync_success(result_data) mock_storage.update_local_storage. \ assert_called_once_with(mock_session, mock_sources, "submissions", "replies", os.path.join(homedir, 'data')) co.update_sources.assert_called_once_with()
def test_Controller_on_file_open(homedir, config, mocker): """ If running on Qubes, a new QProcess with the expected command and args should be started. Using the `config` fixture to ensure the config is written to disk. """ mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) co.proxy = True mock_submission = mocker.MagicMock() mock_submission.filename = '1-test.pdf' mock_subprocess = mocker.MagicMock() mock_process = mocker.MagicMock(return_value=mock_subprocess) mocker.patch('securedrop_client.logic.QProcess', mock_process) co.on_file_open(mock_submission) mock_process.assert_called_once_with(co) mock_subprocess.start.call_count == 1
def test_Controller_update_sources(homedir, config, mocker): """ Ensure the UI displays a list of the available sources from local data store. Using the `config` fixture to ensure the config is written to disk. """ mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) mock_storage = mocker.patch('securedrop_client.logic.storage') source_list = [ factory.Source(last_updated=2), factory.Source(last_updated=1) ] mock_storage.get_local_sources.return_value = source_list co.update_sources() mock_storage.get_local_sources.assert_called_once_with(mock_session) mock_gui.show_sources.assert_called_once_with(source_list)
def test_Controller_on_reply_failure(homedir, mocker): ''' Check that when the result is a failure, the client emits the correct signal. ''' mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) co.api = mocker.Mock() journalist_uuid = 'abc123' co.api.token_journalist_uuid = journalist_uuid mock_reply_succeeded = mocker.patch.object(co, 'reply_succeeded') mock_reply_failed = mocker.patch.object(co, 'reply_failed') source_uuid = 'foo111' msg_uuid = 'bar222' current_object = (source_uuid, msg_uuid) co.on_reply_failure(Exception, current_object) mock_reply_failed.emit.assert_called_once_with(msg_uuid) assert not mock_reply_succeeded.emit.called
def test_Controller_on_file_download_Reply(homedir, config, mocker): """ If the handler is passed a reply, check the download_reply function is the one called against the API. Using the `config` fixture to ensure the config is written to disk. """ mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) source = factory.Source() journalist = db.User('Testy mcTestface') reply = db.Reply(uuid='reply-uuid', journalist=journalist, source=source, filename='1-my-reply.gpg', size=123) # Not a sdclientapi.Submission co.call_api = mocker.MagicMock() co.api = mocker.MagicMock() reply_sdk_object = mocker.MagicMock() mock_reply = mocker.patch('sdclientapi.Reply') mock_reply.return_value = reply_sdk_object co.on_file_download(source, reply) co.call_api.assert_called_once_with(co.api.download_reply, co.on_file_download_success, co.on_file_download_failure, reply_sdk_object, co.data_dir, current_object=reply)
def test_Controller_on_file_download_Submission(homedir, config, mocker): """ If the handler is passed a submission, check the download_submission function is the one called against the API. Using the `config` fixture to ensure the config is written to disk. """ mock_gui = mocker.MagicMock() mock_session = mocker.MagicMock() co = Controller('http://localhost', mock_gui, mock_session, homedir) co.call_api = mocker.MagicMock() co.api = mocker.MagicMock() source = factory.Source() file_ = db.File(source=source, uuid='uuid', size=1234, filename='1-myfile.doc.gpg', download_url='http://myserver/myfile', is_downloaded=False) submission_sdk_object = mocker.MagicMock() mock_submission = mocker.patch('sdclientapi.Submission') mock_submission.return_value = submission_sdk_object co.on_file_download(source, file_) co.call_api.assert_called_once_with( co.api.download_submission, co.on_file_download_success, co.on_file_download_failure, submission_sdk_object, co.data_dir, current_object=file_, )