def test_update_replies_missing_source(homedir, mocker, session): """ Verify that a reply to an invalid source is handled. """ data_dir = os.path.join(homedir, "data") journalist = factory.User(id=1) session.add(journalist) source = factory.Source() session.add(source) # 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 = make_remote_reply("nonexistent-source", journalist.uuid) remote_replies = [remote_reply] local_replies = [] error_logger = mocker.patch("securedrop_client.storage.logger.error") update_replies(remote_replies, local_replies, session, data_dir) error_logger.assert_called_once_with( f"No source found for reply {remote_reply.uuid}")
def test_update_replies_cleanup_drafts(homedir, mocker, session): """ Check that draft replies are deleted if they correspond to a reply fetched from the server. """ data_dir = os.path.join(homedir, "data") # Source object related to the submissions. source = factory.Source() user = factory.User() session.add(source) session.add(user) session.commit() # One reply will exist on the server. remote_reply_create = make_remote_reply(source.uuid, "hehe") remote_replies = [remote_reply_create] # One draft reply will exist in the local database corresponding to the server reply. draft_reply = db.DraftReply( uuid=remote_reply_create.uuid, source=source, journalist=user, file_counter=3, timestamp=datetime.datetime(2000, 6, 6, 6, 0), ) session.add(draft_reply) # Another draft reply will exist that should be moved to _after_ the new reply # once we confirm the previous reply. This ensures consistent ordering of interleaved # drafts (pending and failed) with replies, messages, and files from the user's perspective. draft_reply_new = db.DraftReply( uuid="foo", source=source, journalist=user, file_counter=3, timestamp=datetime.datetime(2001, 6, 6, 6, 0), ) session.add(draft_reply_new) session.commit() # We have no replies locally stored. local_replies = [] update_replies(remote_replies, local_replies, session, data_dir) # Check the expected local source object has been created. new_local_replies = session.query(db.Reply).all() assert len(new_local_replies) == 1 # Check that the only draft is the one sent with uuid 'foo' and its file_counter now # matches the file_counter of the updated reply. This ensures consistent ordering. new_draft_replies = session.query(db.DraftReply).all() assert len(new_draft_replies) == 1 assert new_draft_replies[0].file_counter == new_local_replies[ 0].file_counter assert new_draft_replies[0].uuid == draft_reply_new.uuid
def test_update_replies_deletes_files_associated_with_the_reply( homedir, mocker): """ Check that: * Replies are deleted on disk after sync. """ mock_session = mocker.MagicMock() # Test scenario: one reply locally, no replies on server. remote_replies = [] # A local reply object. To ensure that all files from various # stages of processing are cleaned up, we'll add several filenames. server_filename = "1-pericardial-surfacing-reply.gpg" local_filename_when_decrypted = "1-pericardial-surfacing-reply" local_reply = mocker.MagicMock() local_reply.uuid = "test-uuid" local_reply.filename = server_filename local_reply_source_journalist_filename = "pericardial_surfacing" source_directory = os.path.join(homedir, local_reply_source_journalist_filename) local_reply.location = mocker.MagicMock(return_value=os.path.join( source_directory, local_filename_when_decrypted)) abs_local_filename = add_test_file_to_temp_dir( source_directory, local_filename_when_decrypted) local_replies = [local_reply] # There needs to be a corresponding local_source. local_source = mocker.MagicMock() local_source.uuid = "test-source-uuid" local_source.id = 666 mock_session.query().filter_by.return_value = [local_source] update_replies(remote_replies, local_replies, mock_session, homedir) # Ensure the file associated with the reply are deleted on disk. assert not os.path.exists(abs_local_filename) # Ensure the record for the local reply is gone. mock_session.delete.assert_called_once_with(local_reply) # Session is committed to database. assert mock_session.commit.call_count == 1
def test_update_replies_renames_file_on_disk(homedir, mocker): """ Check that: * Replies are renamed on disk after sync. """ data_dir = os.path.join(homedir, 'data') mock_session = mocker.MagicMock() # Remote replies with new filename server_filename = '1-spotted-potato-reply.gpg' remote_reply = mocker.MagicMock() remote_reply.uuid = 'test-uuid' remote_reply.filename = server_filename remote_replies = [remote_reply] # Local reply that needs to be updated local_filename = '1-pericardial-surfacing-reply.gpg' local_reply = mocker.MagicMock() local_reply.uuid = 'test-uuid' local_reply.filename = local_filename local_replies = [local_reply] # Add reply file to test directory local_filename_decrypted = '1-pericardial-surfacing-reply' add_test_file_to_temp_dir(data_dir, local_filename_decrypted) # There needs to be a corresponding local_source and local_user local_source = mocker.MagicMock() local_source.uuid = 'test-source-uuid' local_source.id = 123 local_user = mocker.MagicMock() local_user.username = '******' local_user.id = 42 mock_focu = mocker.MagicMock(return_value=local_user) mocker.patch('securedrop_client.storage.find_or_create_user', mock_focu) update_replies(remote_replies, local_replies, mock_session, data_dir) updated_local_filename = '1-spotted-potato-reply' assert local_reply.filename == remote_reply.filename assert os.path.exists(os.path.join(data_dir, updated_local_filename))
def test_update_replies(homedir, mocker): """ 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. """ mock_session = mocker.MagicMock() # Source object related to the submissions. source = mocker.MagicMock() source.uuid = str(uuid.uuid4()) # 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) reply_update = make_remote_reply(source.uuid) reply_create = make_remote_reply(source.uuid, 'unknownuser') remote_replies = [reply_update, reply_create] # 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_reply1 = mocker.MagicMock() local_reply1.uuid = reply_update.uuid local_reply1.journalist_uuid = str(uuid.uuid4()) local_reply2 = mocker.MagicMock() local_reply2.uuid = str(uuid.uuid4()) local_reply2.journalist_uuid = str(uuid.uuid4()) local_replies = [local_reply1, local_reply2] # There needs to be a corresponding local_source and local_user local_source = mocker.MagicMock() local_source.uuid = source.uuid local_source.id = 666 # ;-) local_user = mocker.MagicMock() local_user.username = reply_create.journalist_username local_user.id = 42 mock_session.query().filter_by.side_effect = [ [ local_source, ], [ local_user, ], [ local_user, ], ] mock_focu = mocker.MagicMock(return_value=local_user) mocker.patch('securedrop_client.storage.find_or_create_user', mock_focu) update_replies(remote_replies, local_replies, mock_session, homedir) # Check the expected local reply object has been updated with values # from the API. assert local_reply1.journalist_id == local_user.id assert local_reply1.filename == reply_update.filename assert local_reply1.size == reply_update.size # Check the expected local source object has been created with values from # the API. assert mock_session.add.call_count == 1 new_reply = mock_session.add.call_args_list[0][0][0] assert new_reply.uuid == reply_create.uuid assert new_reply.source_id == local_source.id assert new_reply.journalist_id == local_user.id assert new_reply.size == reply_create.size assert new_reply.filename == reply_create.filename # Ensure the record for the local source that is missing from the results # of the API is deleted. mock_session.delete.assert_called_once_with(local_reply2) # Session is committed to database. assert mock_session.commit.call_count == 1
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