def test_ReplyDownloadJob_no_download_or_decrypt(mocker, homedir, session, session_maker): """ Test that an already-downloaded reply successfully decrypts. """ reply_is_decrypted_false = factory.Reply(source=factory.Source(), is_downloaded=True, is_decrypted=False, content=None) reply_is_decrypted_none = factory.Reply(source=factory.Source(), is_downloaded=True, is_decrypted=None, content=None) session.add(reply_is_decrypted_false) session.add(reply_is_decrypted_none) session.commit() gpg = GpgHelper(homedir, session_maker, is_qubes=False) job_1 = ReplyDownloadJob(reply_is_decrypted_false.uuid, homedir, gpg) job_2 = ReplyDownloadJob(reply_is_decrypted_none.uuid, homedir, gpg) mocker.patch.object(job_1.gpg, 'decrypt_submission_or_reply') mocker.patch.object(job_2.gpg, 'decrypt_submission_or_reply') api_client = mocker.MagicMock() api_client.default_request_timeout = mocker.MagicMock() path = os.path.join(homedir, 'data') api_client.download_submission = mocker.MagicMock(return_value=('', path)) job_1.call_api(api_client, session) job_2.call_api(api_client, session) assert reply_is_decrypted_false.content is not None assert reply_is_decrypted_false.is_downloaded is True assert reply_is_decrypted_false.is_decrypted is True assert reply_is_decrypted_none.content is not None assert reply_is_decrypted_none.is_downloaded is True assert reply_is_decrypted_none.is_decrypted is True
def test_find_new_replies(mocker, session): source = factory.Source() reply_not_downloaded = factory.Reply(source=source, is_downloaded=False, is_decrypted=None, content=None) reply_decrypt_not_attempted = factory.Reply(source=source, is_downloaded=True, is_decrypted=None, content=None) reply_decrypt_failed = factory.Reply(source=source, is_downloaded=True, is_decrypted=False, content=None) reply_decrypt_success = factory.Reply(source=source, is_downloaded=True, is_decrypted=True, content="teehee") session.add(source) session.add(reply_decrypt_not_attempted) session.add(reply_not_downloaded) session.add(reply_decrypt_failed) session.add(reply_decrypt_success) session.commit() replies = find_new_replies(session) assert len(replies) == 3 for reply in replies: assert reply.is_downloaded is False or reply.is_decrypted is not True
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_ReplyDownloadJob_happiest_path(mocker, homedir, session, session_maker): """ Test when a reply successfully downloads and decrypts. Use the `homedir` fixture to get a GPG keyring. """ reply = factory.Reply(source=factory.Source(), is_downloaded=False, is_decrypted=None, content=None) session.add(reply) session.commit() gpg = GpgHelper(homedir, session_maker, is_qubes=False) job = ReplyDownloadJob(reply.uuid, homedir, gpg) mocker.patch.object(job.gpg, 'decrypt_submission_or_reply') api_client = mocker.MagicMock() api_client.default_request_timeout = mocker.MagicMock() data_dir = os.path.join(homedir, 'data') api_client.download_reply = mocker.MagicMock(return_value=('', data_dir)) job.call_api(api_client, session) assert reply.content is not None assert reply.is_downloaded is True assert reply.is_decrypted is True
def test_mark_reply_as_downloaded(mocker): session = mocker.MagicMock() reply = factory.Reply(source=factory.Source(), is_downloaded=False) session.query().filter_by().one.return_value = reply mark_as_downloaded(type(reply), "mock_uuid", session) assert reply.is_downloaded is True session.add.assert_called_once_with(reply) session.commit.assert_called_once_with()
def test_get_reply(mocker, session): source = factory.Source() reply = factory.Reply(source=source) session.add(source) session.add(reply) result = get_reply(session, reply.uuid) assert result == reply
def test_reply_with_download_error(session, download_error_codes): r = factory.Reply(is_decrypted=False, content=None) download_error = (session.query(DownloadError).filter_by( name=DownloadErrorCodes.DECRYPTION_ERROR.name).one()) r.download_error = download_error session.commit() classname = r.__class__.__name__.lower() assert str(r) == f"cannot decrypt {classname}"
def test_ReplySync_run_success(mocker, session, source): """Test when a reply successfully downloads and decrypts.""" reply = factory.Reply(source=source['source'], is_downloaded=False, is_decrypted=None, content=None) session.add(reply) session.commit() expected_content = 'foo' def set_object_decryption_status_with_content_side_effect( *nargs, **kwargs): reply.content = expected_content # mock the fetching of replies mocker.patch('securedrop_client.storage.find_new_replies', return_value=[reply]) mock_download_status = mocker.patch( 'securedrop_client.message_sync.storage.mark_reply_as_downloaded') mock_decryption_status = mocker.patch( 'securedrop_client.message_sync.storage.set_object_decryption_status_with_content', side_effect=set_object_decryption_status_with_content_side_effect) # don't create the signal mocker.patch('securedrop_client.message_sync.pyqtSignal') def mock_decrypt_submission_or_reply(filepath, plaintext_filename, is_doc): with open(plaintext_filename, 'w') as f: f.write(expected_content) # mock the GpgHelper creation since we don't have directories/keys setup mock_gpg_helper = mocker.MagicMock( decrypt_submission_or_reply=mock_decrypt_submission_or_reply) mocker.patch('securedrop_client.message_sync.GpgHelper', return_value=mock_gpg_helper) api = mocker.MagicMock(session=session) rs = ReplySync(api, 'mock', True) rs.session = session # "patch" it with a real session rs.api.download_reply = mocker.MagicMock( return_value=(1234, "/home/user/downloads/foo")) mock_reply_ready = mocker.patch.object(rs, 'reply_ready') # check that it runs without raising exceptions rs.run(False) mock_decryption_status.assert_called_once_with(reply, api.session, True, expected_content) mock_download_status.called_once_with(reply, api.mock_session) mock_reply_ready.emit.assert_called_once_with(reply.uuid, expected_content)
def test_reply_already_sent(homedir, mocker, session, session_maker, reply_status_codes): """ Check that if a reply is already sent then we return the reply id. """ source = factory.Source() session.add(source) reply = factory.Reply(source=source) session.add(reply) session.commit() job = SendReplyJob("mock_source_id", reply.uuid, "mock reply message", mocker.MagicMock()) reply_uuid = job.call_api(mocker.MagicMock(), session) assert reply.uuid == reply_uuid
def test_ReplySync_run_decryption_error(mocker, session, source): """Test when a reply successfully downloads, but does not successfully decrypt.""" reply = factory.Reply(source=source['source'], is_downloaded=False, is_decrypted=None, content=None) session.add(reply) session.commit() # mock the fetching of replies mocker.patch('securedrop_client.storage.find_new_replies', return_value=[reply]) mock_download_status = mocker.patch( 'securedrop_client.message_sync.storage.mark_reply_as_downloaded') mock_decryption_status = mocker.patch( 'securedrop_client.message_sync.storage.set_object_decryption_status_with_content' ) # don't create the signal mocker.patch('securedrop_client.message_sync.pyqtSignal') # mock the GpgHelper creation since we don't have directories/keys setup mocker.patch('securedrop_client.message_sync.GpgHelper') api = mocker.MagicMock(session=session) rs = ReplySync(api, 'mock', True) rs.session = session # "patch" it with a real session mocker.patch.object(rs.gpg, 'decrypt_submission_or_reply', side_effect=CryptoError) rs.api.download_reply = mocker.MagicMock( return_value=(1234, "/home/user/downloads/foo")) mock_reply_ready = mocker.patch.object(rs, 'reply_ready') # check that it runs without raising exceptions rs.run(False) mock_download_status.assert_called_once_with(reply.uuid, session) mock_decryption_status.assert_called_once_with(reply, api.session, False) mock_reply_ready.emit.assert_called_once_with(reply.uuid, '<Reply not yet available>')
def test_ReplyDownloadJob_message_already_decrypted(mocker, homedir, session, session_maker): """ Test that call_api just returns uuid if already decrypted. """ reply = factory.Reply(source=factory.Source(), is_downloaded=True, is_decrypted=True) session.add(reply) session.commit() gpg = GpgHelper(homedir, session_maker, is_qubes=False) job = ReplyDownloadJob(reply.uuid, homedir, gpg) decrypt_fn = mocker.patch.object(job.gpg, 'decrypt_submission_or_reply') api_client = mocker.MagicMock() download_fn = mocker.patch.object(api_client, 'download_reply') return_uuid = job.call_api(api_client, session) assert reply.uuid == return_uuid decrypt_fn.assert_not_called() download_fn.assert_not_called()
def test_ReplySync_exception(mocker): """ Mostly here for code coverage- makes sure that if an exception is raised in the download thread, the code which catches it is actually run """ reply = factory.Reply() api = mocker.MagicMock() home = "/home/user/.sd" is_qubes = False mocker.patch('securedrop_client.storage.find_new_replies', return_value=[reply]) mocker.patch('securedrop_client.message_sync.GpgHelper') mocker.patch("sdclientapi.sdlocalobjects.Reply", mocker.MagicMock(side_effect=Exception())) rs = ReplySync(api, home, is_qubes) # check that it runs without raise exceptions rs.run(False)
def test_ReplySync_run_failure(mocker, session, source): reply = factory.Reply(source=source['source']) session.add(reply) session.commit() # mock finding new replies mocker.patch('securedrop_client.storage.find_new_replies', return_value=[reply]) # mock handling the new reply mocker.patch( 'securedrop_client.message_sync.storage.mark_reply_as_downloaded') mocker.patch('securedrop_client.message_sync.GpgHelper') api = mocker.MagicMock() home = "/home/user/.sd" is_qubes = False ms = ReplySync(api, home, is_qubes) ms.api.download_submission = mocker.MagicMock( return_value=(1234, "/home/user/downloads/foo")) # check that it runs without raise exceptions ms.run(False)
def test_update_replies(homedir, mocker, session): """ Check that: * Existing replies are updated in the local database. * New replies have an entry in the local database. * Local replies not returned by the remote server are deleted from the local database. * References to journalist's usernames are correctly handled. """ data_dir = os.path.join(homedir, "data") journalist = factory.User(id=1) session.add(journalist) source = factory.Source() session.add(source) # Some local reply objects. One already exists in the API results # (this will be updated), one does NOT exist in the API results (this will # be deleted from the local database). local_reply_update = factory.Reply( source_id=source.id, source=source, journalist_id=journalist.id, filename="1-original-reply.gpg.", size=2, ) session.add(local_reply_update) local_reply_delete = factory.Reply(source_id=source.id, source=source) session.add(local_reply_delete) local_replies = [local_reply_update, local_reply_delete] # Some remote reply objects from the API, one of which will exist in the # local database, the other will NOT exist in the local database # (this will be added to the database) remote_reply_update = factory.RemoteReply( journalist_uuid=journalist.uuid, uuid=local_reply_update.uuid, source_url="/api/v1/sources/{}".format(source.uuid), file_counter=local_reply_update.file_counter, filename=local_reply_update.filename, ) remote_reply_create = factory.RemoteReply( journalist_uuid=journalist.uuid, source_url="/api/v1/sources/{}".format(source.uuid), file_counter=factory.REPLY_COUNT + 1, filename="{}-filename.gpg".format(factory.REPLY_COUNT + 1), ) remote_replies = [remote_reply_update, remote_reply_create] session.commit() update_replies(remote_replies, local_replies, session, data_dir) session.commit() # Check the expected local reply object has been updated with values # from the API. assert local_reply_update.journalist_id == journalist.id assert local_reply_update.size == remote_reply_update.size assert local_reply_update.filename == remote_reply_update.filename new_reply = session.query( db.Reply).filter_by(uuid=remote_reply_create.uuid).one() assert new_reply.source_id == source.id assert new_reply.journalist_id == journalist.id assert new_reply.size == remote_reply_create.size assert new_reply.filename == remote_reply_create.filename # Ensure the local reply that is not in the API results is deleted. assert session.query( db.Reply).filter_by(uuid=local_reply_delete.uuid).count() == 0