def test_send_reply_failure_unknown_error( homedir, mocker, session, session_maker, reply_status_codes ): """ Check that if the SendReplyJob api call fails when sending a message that SendReplyJobError is raised and the reply is not added to the local database. """ source = factory.Source() session.add(source) draft_reply = factory.DraftReply(uuid="mock_reply_uuid") session.add(draft_reply) session.commit() api_client = mocker.MagicMock() mocker.patch.object(api_client, "reply_source", side_effect=Exception) gpg = GpgHelper(homedir, session_maker, is_qubes=False) encrypt_fn = mocker.patch.object(gpg, "encrypt_to_source") job = SendReplyJob(source.uuid, "mock_reply_uuid", "mock_message", gpg) with pytest.raises(Exception): job.call_api(api_client, session) encrypt_fn.assert_called_once_with(source.uuid, "mock_message") replies = session.query(db.Reply).filter_by(uuid="mock_reply_uuid").all() assert len(replies) == 0 # Ensure that the draft reply is still in the db drafts = session.query(db.DraftReply).filter_by(uuid="mock_reply_uuid").all() assert len(drafts) == 1
def test_send_reply_failure_timeout_error( homedir, mocker, session, session_maker, reply_status_codes, exception ): """ Check that if the SendReplyJob api call fails because of a RequestTimeoutError or ServerConnectionError that a SendReplyJobTimeoutError is raised. """ source = factory.Source() session.add(source) draft_reply = factory.DraftReply(uuid="mock_reply_uuid") session.add(draft_reply) session.commit() api_client = mocker.MagicMock() mocker.patch.object(api_client, "reply_source", side_effect=exception) gpg = GpgHelper(homedir, session_maker, is_qubes=False) encrypt_fn = mocker.patch.object(gpg, "encrypt_to_source") job = SendReplyJob(source.uuid, "mock_reply_uuid", "mock_message", gpg) with pytest.raises(SendReplyJobTimeoutError): job.call_api(api_client, session) encrypt_fn.assert_called_once_with(source.uuid, "mock_message") replies = session.query(db.Reply).filter_by(uuid="mock_reply_uuid").all() assert len(replies) == 0 # Ensure that the draft reply is still in the db drafts = session.query(db.DraftReply).filter_by(uuid="mock_reply_uuid").all() assert len(drafts) == 1
def test_send_reply_failure_gpg_error(homedir, mocker, session, session_maker, reply_status_codes): ''' Check that if gpg fails when sending a message, we do not call the API, and ensure that SendReplyJobError is raised when there is a CryptoError so we can handle it in ApiJob._do_call_api. ''' source = factory.Source() session.add(source) msg_uuid = 'xyz456' draft_reply = factory.DraftReply(uuid=msg_uuid) session.add(draft_reply) session.commit() gpg = GpgHelper(homedir, session_maker, is_qubes=False) api_client = mocker.MagicMock() api_client.token_journalist_uuid = 'journalist ID sending the reply' mock_encrypt = mocker.patch.object(gpg, 'encrypt_to_source', side_effect=CryptoError) msg = 'wat' mock_reply_response = sdclientapi.Reply(uuid=msg_uuid, filename='5-dummy-reply') api_client.reply_source = mocker.MagicMock() api_client.reply_source.return_value = mock_reply_response mock_sdk_source = mocker.Mock() mock_source_init = mocker.patch( 'securedrop_client.logic.sdclientapi.Source', return_value=mock_sdk_source) job = SendReplyJob( source.uuid, msg_uuid, msg, gpg, ) with pytest.raises(SendReplyJobError): job.call_api(api_client, session) # Ensure we attempted to encrypt the message mock_encrypt.assert_called_once_with(source.uuid, msg) assert mock_source_init.call_count == 0 # Ensure reply did not get added to db replies = session.query(db.Reply).filter_by(uuid=msg_uuid).all() assert len(replies) == 0 # Ensure that the draft reply is still in the db drafts = session.query(db.DraftReply).filter_by(uuid=msg_uuid).all() assert len(drafts) == 1
def test_send_reply_success(homedir, mocker, session, session_maker, reply_status_codes): ''' Check that the "happy path" of encrypting a message and sending it to the server behaves as expected. ''' source = factory.Source() session.add(source) msg_uuid = 'xyz456' draft_reply = factory.DraftReply(uuid=msg_uuid) session.add(draft_reply) session.commit() gpg = GpgHelper(homedir, session_maker, is_qubes=False) api_client = mocker.MagicMock() api_client.token_journalist_uuid = 'journalist ID sending the reply' encrypted_reply = 's3kr1t m3ss1dg3' mock_encrypt = mocker.patch.object(gpg, 'encrypt_to_source', return_value=encrypted_reply) msg = 'wat' mock_reply_response = sdclientapi.Reply(uuid=msg_uuid, filename='5-dummy-reply') api_client.reply_source = mocker.MagicMock() api_client.reply_source.return_value = mock_reply_response mock_sdk_source = mocker.Mock() mock_source_init = mocker.patch( 'securedrop_client.logic.sdclientapi.Source', return_value=mock_sdk_source) job = SendReplyJob( source.uuid, msg_uuid, msg, gpg, ) job.call_api(api_client, session) # ensure message gets encrypted mock_encrypt.assert_called_once_with(source.uuid, msg) mock_source_init.assert_called_once_with(uuid=source.uuid) # assert reply got added to db reply = session.query(db.Reply).filter_by(uuid=msg_uuid).one() assert reply.journalist_id == api_client.token_journalist_uuid
def send_reply(self, source_uuid: str, reply_uuid: str, message: str) -> None: """ Send a reply to a source. """ # Before we send the reply, add the draft to the database with a PENDING # reply send status. source = self.session.query(db.Source).filter_by(uuid=source_uuid).one() reply_status = self.session.query(db.ReplySendStatus).filter_by( name=db.ReplySendStatusCodes.PENDING.value).one() draft_reply = db.DraftReply( uuid=reply_uuid, timestamp=datetime.datetime.utcnow(), source_id=source.id, journalist_id=self.api.token_journalist_uuid, file_counter=source.interaction_count, content=message, send_status_id=reply_status.id, ) self.session.add(draft_reply) self.session.commit() job = SendReplyJob(source_uuid, reply_uuid, message, self.gpg) job.success_signal.connect(self.on_reply_success, type=Qt.QueuedConnection) job.failure_signal.connect(self.on_reply_failure, type=Qt.QueuedConnection) self.api_job_queue.enqueue(job)
def test_send_reply_sql_exception_during_failure( homedir, mocker, session, session_maker, reply_status_codes ): """ Check that we do not raise an unhandled exception when we set the draft reply status to failed in the except block if there is a SQL exception. """ source = factory.Source() session.add(source) # Note that we do not add a DraftReply. An exception will occur when we try # to set the reply status to 'FAILED' for a non-existent reply, which we # expect to be handled. gpg = GpgHelper(homedir, session_maker, is_qubes=False) job = SendReplyJob(source.uuid, "mock_reply_uuid", "mock_message", gpg) # This should not raise an exception job._set_status_to_failed(session)
def test_send_reply_unexpected_exception_during_failure( homedir, mocker, session, session_maker, reply_status_codes): ''' Check that we do not raise an unhandled exception when we set the draft reply status to failed in the except block if there is an unexpected exception. ''' source = factory.Source() session.add(source) draft_reply = factory.DraftReply(uuid='mock_reply_uuid') session.add(draft_reply) session.commit() # session.commit() is called when we try to set the status to failed. session.commit = mocker.MagicMock(side_effect=Exception("BOOM")) gpg = GpgHelper(homedir, session_maker, is_qubes=False) job = SendReplyJob(source.uuid, 'mock_reply_uuid', 'mock_message', gpg) # This should not raise an exception job._set_status_to_failed(session)
def test_send_reply_failure_when_repr_is_none( homedir, mocker, session, session_maker, reply_status_codes ): """ Check that the SendReplyJob api call results in a SendReplyJobError and nothing else, e.g. no TypeError, when an api call results in an exception that returns None for __repr__ (regression test). """ class MockException(Exception): def __repr__(self): return None source = factory.Source(uuid="mock_reply_uuid") session.add(source) draft_reply = factory.DraftReply(uuid="mock_reply_uuid") session.add(draft_reply) session.commit() api_client = mocker.MagicMock() mocker.patch.object(api_client, "reply_source", side_effect=MockException("mock")) gpg = GpgHelper(homedir, session_maker, is_qubes=False) encrypt_fn = mocker.patch.object(gpg, "encrypt_to_source") job = SendReplyJob(source.uuid, "mock_reply_uuid", "mock_message", gpg) error = "Failed to send reply mock_reply_uuid for source {} due to Exception: mock".format( source.uuid ) with pytest.raises(SendReplyJobError, match=error): job.call_api(api_client, session) encrypt_fn.assert_called_once_with(source.uuid, "mock_message") replies = session.query(db.Reply).filter_by(uuid="mock_reply_uuid").all() assert len(replies) == 0 # Ensure that the draft reply is still in the db drafts = session.query(db.DraftReply).filter_by(uuid="mock_reply_uuid").all() assert len(drafts) == 1