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
Exemple #4
0
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))
Exemple #5
0
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