diff --git a/tests/api_jobs/test_uploads.py b/tests/api_jobs/test_uploads.py index 5f335c0c07..4641335d33 100644 --- a/tests/api_jobs/test_uploads.py +++ b/tests/api_jobs/test_uploads.py @@ -13,13 +13,17 @@ def test_SendReplyJobTimeoutError(): assert str(error) == 'mock_message' -def test_send_reply_success(homedir, mocker, session, session_maker): +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) @@ -29,7 +33,6 @@ def test_send_reply_success(homedir, mocker, session, session_maker): encrypted_reply = 's3kr1t m3ss1dg3' mock_encrypt = mocker.patch.object(gpg, 'encrypt_to_source', return_value=encrypted_reply) - msg_uuid = 'xyz456' msg = 'wat' mock_reply_response = sdclientapi.Reply(uuid=msg_uuid, filename='5-dummy-reply') @@ -58,7 +61,8 @@ def test_send_reply_success(homedir, mocker, session, session_maker): assert reply.journalist_id == api_client.token_journalist_uuid -def test_send_reply_failure_gpg_error(homedir, mocker, session, session_maker): +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 @@ -66,6 +70,9 @@ def test_send_reply_failure_gpg_error(homedir, mocker, session, session_maker): ''' 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) @@ -74,7 +81,6 @@ def test_send_reply_failure_gpg_error(homedir, mocker, session, session_maker): api_client.token_journalist_uuid = 'journalist ID sending the reply' mock_encrypt = mocker.patch.object(gpg, 'encrypt_to_source', side_effect=CryptoError) - msg_uuid = 'xyz456' msg = 'wat' mock_reply_response = sdclientapi.Reply(uuid=msg_uuid, filename='5-dummy-reply') @@ -103,14 +109,21 @@ def test_send_reply_failure_gpg_error(homedir, mocker, session, session_maker): 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_failure_unknown_error(homedir, mocker, session, session_maker): + +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) @@ -125,14 +138,21 @@ def test_send_reply_failure_unknown_error(homedir, mocker, session, session_make 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): +def test_send_reply_failure_timeout_error(homedir, mocker, session, session_maker, + reply_status_codes): ''' Check that if the SendReplyJob api call fails because of a RequestTimeoutError 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=sdclientapi.RequestTimeoutError) @@ -147,8 +167,13 @@ def test_send_reply_failure_timeout_error(homedir, mocker, session, session_make 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_when_repr_is_none(homedir, mocker, session, session_maker): + +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__ @@ -160,6 +185,8 @@ def __repr__(self): 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')) @@ -175,3 +202,7 @@ def __repr__(self): 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 diff --git a/tests/factory.py b/tests/factory.py index a96d835fb9..9e4d2063a5 100644 --- a/tests/factory.py +++ b/tests/factory.py @@ -12,6 +12,7 @@ MESSAGE_COUNT = 0 FILE_COUNT = 0 REPLY_COUNT = 0 +DRAFT_REPLY_COUNT = 0 USER_COUNT = 0 @@ -82,6 +83,23 @@ def Reply(**attrs): return db.Reply(**defaults) +def DraftReply(**attrs): + global DRAFT_REPLY_COUNT + DRAFT_REPLY_COUNT += 1 + defaults = dict( + timestamp=datetime.utcnow(), + source_id=1, + journalist_id=1, + file_counter=1, + uuid='draft-reply-uuid-{}'.format(REPLY_COUNT), + content='content', + ) + + defaults.update(attrs) + + return db.DraftReply(**defaults) + + def File(**attrs): global FILE_COUNT FILE_COUNT += 1 diff --git a/tests/gui/test_widgets.py b/tests/gui/test_widgets.py index 97a31b5977..f14848cd8b 100644 --- a/tests/gui/test_widgets.py +++ b/tests/gui/test_widgets.py @@ -1236,6 +1236,7 @@ def test_ReplyWidget_init(mocker): ReplyWidget( 'mock id', 'hello', + 'dummy', mock_update_signal, mock_success_signal, mock_failure_signal, @@ -1857,7 +1858,7 @@ def test_ConversationView_add_reply_from_reply_box(mocker): cv.add_reply_from_reply_box('abc123', 'test message') reply_widget.assert_called_once_with( - 'abc123', 'test message', reply_ready, reply_succeeded, reply_failed) + 'abc123', 'test message', 'PENDING', reply_ready, reply_succeeded, reply_failed) cv.conversation_layout.addWidget.assert_called_once_with( reply_widget_res, alignment=Qt.AlignRight) @@ -1895,6 +1896,7 @@ def test_ConversationView_add_reply(mocker, session, source): mock_reply_widget.assert_called_once_with( reply.uuid, content, + 'SUCCEEDED', mock_reply_ready_signal, mock_reply_succeeded_signal, mock_reply_failed_signal) @@ -1938,6 +1940,7 @@ def test_ConversationView_add_reply_no_content(mocker, session, source): mock_reply_widget.assert_called_once_with( reply.uuid, '', + 'SUCCEEDED', mock_reply_ready_signal, mock_reply_succeeded_signal, mock_reply_failed_signal) @@ -2244,6 +2247,7 @@ def test_ReplyWidget_success_failure_slots(mocker): widget = ReplyWidget(msg_id, 'lol', + 'PENDING', mock_update_signal, mock_success_signal, mock_failure_signal) diff --git a/tests/test_logic.py b/tests/test_logic.py index 882b0704e1..95547ad0d4 100644 --- a/tests/test_logic.py +++ b/tests/test_logic.py @@ -1220,12 +1220,14 @@ def test_Controller_delete_source(homedir, config, mocker, session_maker): ) -def test_Controller_send_reply_success(homedir, config, mocker, session_maker, session): +def test_Controller_send_reply_success(homedir, config, mocker, session_maker, session, + reply_status_codes): ''' Check that a SendReplyJob is submitted to the queue when send_reply is called. ''' mock_gui = mocker.MagicMock() co = Controller('http://localhost', mock_gui, session_maker, homedir) + co.user = factory.User() mock_success_signal = mocker.MagicMock() mock_failure_signal = mocker.MagicMock() @@ -1237,9 +1239,9 @@ def test_Controller_send_reply_success(homedir, config, mocker, session_maker, s source = factory.Source() session.add(source) + msg_uuid = 'xyz456' session.commit() - msg_uuid = 'xyz456' msg = 'wat' co.send_reply(source.uuid, msg_uuid, msg) diff --git a/tests/test_models.py b/tests/test_models.py index 2178d94310..0c6c5b03e3 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,8 @@ +import datetime import pytest from tests import factory -from securedrop_client.db import Reply, File, Message, User +from securedrop_client.db import DraftReply, Reply, File, Message, ReplySendStatus, User def test_user_fullname(): @@ -72,6 +73,18 @@ def test_string_representation_of_reply(): reply.__repr__() +def test_string_representation_of_draft_reply(): + user = User(username='hehe') + source = factory.Source() + draft_reply = DraftReply(source=source, journalist=user, uuid='test') + draft_reply.__repr__() + + +def test_string_representation_of_send_reply_status(): + reply_status = ReplySendStatus(name='teehee') + reply_status.__repr__() + + def test_source_collection(): # Create some test submissions and replies source = factory.Source() @@ -92,6 +105,36 @@ def test_source_collection(): assert source.collection[2] == message +def test_source_collection_ordering_with_multiple_draft_replies(): + # Create some test submissions, replies, and draft replies. + source = factory.Source() + file_1 = File(source=source, uuid="test", size=123, filename="1-test.doc.gpg", + download_url='http://test/test') + message_2 = Message(source=source, uuid="test", size=123, filename="2-test.doc.gpg", + download_url='http://test/test') + user = User(username='hehe') + reply_3 = Reply(source=source, journalist=user, filename="3-reply.gpg", + size=1234, uuid='test') + draft_reply_4 = DraftReply(uuid='4', source=source, journalist=user, file_counter=3, + timestamp=datetime.datetime(2001, 6, 6, 6, 0)) + draft_reply_5 = DraftReply(uuid='5', source=source, journalist=user, file_counter=3, + timestamp=datetime.datetime(2000, 6, 6, 6, 0)) + reply_6 = Reply(source=source, journalist=user, filename="4-reply.gpg", + size=1234, uuid='test2') + source.files = [file_1] + source.messages = [message_2] + source.replies = [reply_3, reply_6] + source.draftreplies = [draft_reply_4, draft_reply_5] + + # Now these items should be in the source collection in the proper order + assert source.collection[0] == file_1 + assert source.collection[1] == message_2 + assert source.collection[2] == reply_3 + assert source.collection[3] == draft_reply_4 + assert source.collection[4] == draft_reply_5 + assert source.collection[5] == reply_6 + + def test_file_init(): ''' Check that: