Пример #1
0
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()
Пример #2
0
def test_Controller_completed_api_call_with_current_object(
        homedir, config, mocker):
    """
    Ensure that cleanup is performed if an API call returns in the expected
    time.
    Using the `config` fixture to ensure the config is written to disk.
    """
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    mock_thread = mocker.MagicMock()
    mock_runner = mocker.MagicMock()
    mock_runner.result = 'result'
    mock_runner.current_object = 'current_object'
    co.api_threads = {
        'thread_uuid': {
            'thread': mock_thread,
            'runner': mock_runner,
        }
    }
    mock_user_callback = mocker.MagicMock()

    mock_arg_spec = mocker.MagicMock(args=['foo', 'current_object'])
    mocker.patch('securedrop_client.logic.inspect.getfullargspec',
                 return_value=mock_arg_spec)

    co.completed_api_call('thread_uuid', mock_user_callback)
    mock_user_callback.assert_called_once_with('result',
                                               current_object='current_object')
Пример #3
0
def test_Controller_is_authenticated_property(homedir, mocker):
    '''
    Check that the @property `is_authenticated`:
      - Cannot be deleted
      - Emits the correct signals when updated
      - Sets internal state to ensure signals are only set when the state changes
    '''
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()

    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    mock_signal = mocker.patch.object(co, 'authentication_state')

    # default state is unauthenticated
    assert co.is_authenticated is False

    # the property cannot be deleted
    with pytest.raises(AttributeError):
        del co.is_authenticated

    # setting the signal to its current value does not fire the signal
    co.is_authenticated = False
    assert not mock_signal.emit.called
    assert co.is_authenticated is False

    # setting the property to True sends a signal
    co.is_authenticated = True
    mock_signal.emit.assert_called_once_with(True)
    assert co.is_authenticated is True

    mock_signal.reset_mock()

    co.is_authenticated = False
    mock_signal.emit.assert_called_once_with(False)
    assert co.is_authenticated is False
Пример #4
0
def test_Controller_on_reply_success(homedir, mocker):
    '''
    Check that when the result is a success, the client emits the correct signal.
    '''
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    mock_reply_init = mocker.patch('securedrop_client.logic.db.Reply')

    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    co.api = mocker.Mock()
    journalist_uuid = 'abc123'
    co.api.token_journalist_uuid = journalist_uuid
    mock_reply_succeeded = mocker.patch.object(co, 'reply_succeeded')
    mock_reply_failed = mocker.patch.object(co, 'reply_failed')

    reply = sdlocalobjects.Reply(uuid='xyz456', filename='1-wat.gpg')

    source_uuid = 'foo111'
    msg_uuid = 'bar222'
    current_object = (source_uuid, msg_uuid)
    co.on_reply_success(reply, current_object)
    co.session.commit.assert_called_once_with()
    mock_reply_succeeded.emit.assert_called_once_with(msg_uuid)
    assert not mock_reply_failed.emit.called

    assert mock_reply_init.called  # to prevent stale mocks
Пример #5
0
def test_Controller_call_api(homedir, config, mocker):
    """
    A new thread and APICallRunner is created / setup.
    Using the `config` fixture to ensure the config is written to disk.
    """
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    co.finish_api_call = mocker.MagicMock()
    mocker.patch('securedrop_client.logic.QThread')
    mocker.patch('securedrop_client.logic.APICallRunner')
    mocker.patch('securedrop_client.logic.QTimer')
    mock_api_call = mocker.MagicMock()
    mock_success_callback = mocker.MagicMock()
    mock_failure_callback = mocker.MagicMock()

    co.call_api(mock_api_call,
                mock_success_callback,
                mock_failure_callback,
                'foo',
                bar='baz')

    assert len(co.api_threads) == 1
    thread_info = co.api_threads[list(co.api_threads.keys())[0]]
    thread = thread_info['thread']
    runner = thread_info['runner']
    thread.started.connect.assert_called_once_with(runner.call_api)
    thread.start.assert_called_once_with()
    runner.moveToThread.assert_called_once_with(thread)
    runner.call_succeeded.connect.call_count == 1
    runner.call_failed.connect.call_count == 1
    runner.call_timed_out.connect.call_count == 1
Пример #6
0
def test_Controller_authenticated_no_api(homedir, config, mocker):
    """
    If the API is authenticated return True.
    Using the `config` fixture to ensure the config is written to disk.
    """
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    co.api = None
    assert co.authenticated() is False
Пример #7
0
def test_Controller_last_sync_no_file(homedir, config, mocker):
    """
    If there's no sync file, then just return None.
    Using the `config` fixture to ensure the config is written to disk.
    """
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    mocker.patch("builtins.open", mocker.MagicMock(side_effect=Exception()))
    assert co.last_sync() is None
Пример #8
0
def test_Controller_api_call_timeout(homedir, config, mocker):
    '''
    Using the `config` fixture to ensure the config is written to disk.
    '''
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    co.on_api_timeout()
    mock_gui.update_error_status.assert_called_once_with(
        'The connection to the SecureDrop server timed out. Please try again.')
Пример #9
0
def test_Controller_on_file_downloaded_api_failure(homedir, config, mocker):
    '''
    Using the `config` fixture to ensure the config is written to disk.
    '''
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    co.file_ready = mocker.MagicMock()  # signal when file is downloaded
    co.update_sources = mocker.MagicMock()
    co.api_runner = mocker.MagicMock()

    test_filename = "1-my-file-location-msg.gpg"
    co.api_runner.result = ("", test_filename)
    co.call_reset = mocker.MagicMock()
    co.set_status = mocker.MagicMock()
    result_data = Exception('error message')
    submission_db_object = mocker.MagicMock()
    submission_db_object.uuid = 'myuuid'
    submission_db_object.filename = 'filename'

    co.on_file_download_failure(result_data,
                                current_object=submission_db_object)

    co.set_status.assert_called_once_with(
        "The file download failed. Please try again.")
    co.file_ready.emit.assert_not_called()
Пример #10
0
def test_Controller_set_activity_status(homedir, config, mocker):
    """
    Ensure the GUI set_status API is called.
    Using the `config` fixture to ensure the config is written to disk.
    """
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    co.set_status("Hello, World!", 1000)
    mock_gui.update_activity_status.assert_called_once_with(
        "Hello, World!", 1000)
Пример #11
0
def test_Controller_on_delete_source_failure(homedir, config, mocker):
    '''
    Using the `config` fixture to ensure the config is written to disk.
    '''
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    co.sync_api = mocker.MagicMock()
    co.on_delete_source_failure(Exception())
    co.gui.update_error_status.assert_called_with(
        'Failed to delete source at server')
Пример #12
0
def test_Controller_on_delete_source_success(homedir, config, mocker):
    '''
    Using the `config` fixture to ensure the config is written to disk.
    '''
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    co.sync_api = mocker.MagicMock()
    co.on_delete_source_success(True)
    co.sync_api.assert_called_with()
    co.gui.clear_error_status.assert_called_with()
Пример #13
0
def test_Controller_on_action_requiring_login(homedir, config, mocker):
    """
    Ensure that when on_action_requiring_login is called, an error
    is shown in the GUI status area.
    Using the `config` fixture to ensure the config is written to disk.
    """
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    co.on_action_requiring_login()
    mock_gui.update_error_status.assert_called_once_with(
        'You must sign in to perform this action.')
Пример #14
0
def test_Controller_setup(homedir, config, mocker):
    """
    Ensure the application is set up with the following default state:
    Using the `config` fixture to ensure the config is written to disk.
    """
    co = Controller('http://localhost', mocker.MagicMock(), mocker.MagicMock(),
                    homedir)
    co.update_sources = mocker.MagicMock()

    co.setup()

    co.gui.setup.assert_called_once_with(co)
Пример #15
0
def test_Controller_on_authenticate_failure(homedir, config, mocker):
    """
    If the server responds with a negative to the request to authenticate, make
    sure the user knows.
    Using the `config` fixture to ensure the config is written to disk.
    """
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    result_data = Exception('oh no')
    co.on_authenticate_failure(result_data)
    mock_gui.show_login_error.\
        assert_called_once_with(error='There was a problem signing in. Please '
                                'verify your credentials and try again.')
Пример #16
0
def test_Controller_on_file_downloaded_success(homedir, config, mocker):
    '''
    Using the `config` fixture to ensure the config is written to disk.
    '''
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    co.update_sources = mocker.MagicMock()
    co.api_runner = mocker.MagicMock()
    co.file_ready = mocker.MagicMock()  # signal when file is downloaded

    test_filename = "1-my-file-location-msg.gpg"
    test_object_uuid = 'uuid-of-downloaded-object'
    co.call_reset = mocker.MagicMock()
    result_data = ('this-is-a-sha256-sum', test_filename)
    submission_db_object = mocker.MagicMock()
    submission_db_object.uuid = test_object_uuid
    submission_db_object.filename = test_filename
    mock_storage = mocker.patch('securedrop_client.logic.storage')
    mock_gpg = mocker.patch.object(co.gpg,
                                   'decrypt_submission_or_reply',
                                   return_value='filepath')
    mocker.patch('shutil.move')

    co.on_file_download_success(result_data,
                                current_object=submission_db_object)

    mock_gpg.call_count == 1
    mock_storage.mark_file_as_downloaded.assert_called_once_with(
        test_object_uuid, mock_session)
    mock_storage.set_object_decryption_status_with_content.assert_called_once_with(
        submission_db_object, mock_session, True)

    # Signal should be emitted with UUID of the successfully downloaded object
    co.file_ready.emit.assert_called_once_with(test_object_uuid)
Пример #17
0
def test_Controller_unstars_a_source_if_unstarred(homedir, config, mocker):
    """
    Ensure that the client stars a source if it is unstarred.
    Using the `config` fixture to ensure the config is written to disk.
    """
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)

    source_db_object = mocker.MagicMock()
    source_db_object.uuid = mocker.MagicMock()
    source_db_object.is_starred = False

    co.call_api = mocker.MagicMock()
    co.api = mocker.MagicMock()
    co.api.add_star = mocker.MagicMock()
    co.on_update_star_success = mocker.MagicMock()
    co.on_update_star_failure = mocker.MagicMock()

    source_sdk_object = mocker.MagicMock()
    mock_source = mocker.patch('sdclientapi.Source')
    mock_source.return_value = source_sdk_object
    co.update_star(source_db_object)

    co.call_api.assert_called_once_with(
        co.api.add_star,
        co.on_update_star_success,
        co.on_update_star_failure,
        source_sdk_object,
    )
    mock_gui.clear_error_status.assert_called_once_with()
Пример #18
0
def test_Controller_on_sync_failure(homedir, config, mocker):
    """
    If there's no result to syncing, then don't attempt to update local storage
    and perhaps implement some as-yet-undefined UI update.
    Using the `config` fixture to ensure the config is written to disk.
    """
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    co.update_sources = mocker.MagicMock()
    result_data = Exception('Boom')  # Not the expected tuple.
    mock_storage = mocker.patch('securedrop_client.logic.storage')
    co.on_sync_failure(result_data)
    assert mock_storage.update_local_storage.call_count == 0
    co.update_sources.assert_called_once_with()
Пример #19
0
def test_Controller_start_reply_thread(homedir, config, mocker):
    """
    When starting reply-fetching thread, make sure we do a few things.
    Using the `config` fixture to ensure the config is written to disk.
    """
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    mock_qthread = mocker.patch('securedrop_client.logic.QThread')
    mocker.patch('securedrop_client.logic.ReplySync')
    co.reply_sync = mocker.MagicMock()
    co.start_reply_thread()
    co.reply_sync.moveToThread.assert_called_once_with(mock_qthread())
    co.reply_thread.started.connect.assert_called_once_with(co.reply_sync.run)
    co.reply_thread.start.assert_called_once_with()
Пример #20
0
def test_Controller_login(homedir, config, mocker):
    """
    Ensures the API is called in the expected manner for logging in the user
    given the username, password and 2fa token.
    Using the `config` fixture to ensure the config is written to disk.
    """
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    co.call_api = mocker.MagicMock()
    mock_api = mocker.patch('securedrop_client.logic.sdclientapi.API')
    co.login('username', 'password', '123456')
    co.call_api.assert_called_once_with(mock_api().authenticate,
                                        co.on_authenticate_success,
                                        co.on_authenticate_failure)
Пример #21
0
def test_Controller_last_sync_with_file(homedir, config, mocker):
    """
    The flag indicating the time of the last sync with the API is stored in a
    dotfile in the user's home directory. If such a file exists, ensure an
    "arrow" object (representing the date/time) is returned.
    Using the `config` fixture to ensure the config is written to disk.
    """
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    timestamp = '2018-10-10 18:17:13+01:00'
    mocker.patch("builtins.open", mocker.mock_open(read_data=timestamp))
    result = co.last_sync()
    assert isinstance(result, arrow.Arrow)
    assert result.format() == timestamp
Пример #22
0
def modal_dialog(mocker, homedir):
    app = QApplication([])
    gui = Window()
    gui.show()
    controller = Controller("http://localhost", gui, mocker.MagicMock(), homedir, proxy=False)
    controller.qubes = False
    gui.setup(controller)
    gui.login_dialog.close()
    app.setActiveWindow(gui)
    dialog = ModalDialog()

    yield dialog

    dialog.close()
    app.exit()
Пример #23
0
def start_app(args, qt_args) -> None:
    """
    Create all the top-level assets for the application, set things up and
    run the application. Specific tasks include:

    - set up locale and language.
    - set up logging.
    - create an application object.
    - create a window for the app.
    - create an API connection to the SecureDrop proxy.
    - create a SqlAlchemy session to local storage.
    - configure the client (logic) object.
    - ensure the application is setup in the default safe starting state.
    """
    configure_locale_and_language()
    init(args.sdc_home)
    configure_logging(args.sdc_home)
    logging.info('Starting SecureDrop Client {}'.format(__version__))

    app = QApplication(qt_args)
    app.setApplicationName('SecureDrop Client')
    app.setDesktopFileName('org.freedomofthepress.securedrop.client')
    app.setApplicationVersion(__version__)
    app.setAttribute(Qt.AA_UseHighDpiPixmaps)

    load_font('Montserrat')
    load_font('Source_Sans_Pro')

    prevent_second_instance(app, args.sdc_home)

    session_maker = make_session_maker(args.sdc_home)

    gui = Window()

    app.setWindowIcon(load_icon(gui.icon))
    app.setStyleSheet(load_css('sdclient.css'))

    controller = Controller("http://localhost:8081/", gui, session_maker,
                            args.sdc_home, not args.no_proxy,
                            not args.no_qubes)
    controller.setup()

    configure_signal_handlers(app)
    timer = QTimer()
    timer.start(500)
    timer.timeout.connect(lambda: None)

    sys.exit(app.exec_())
Пример #24
0
def export_dialog(mocker, homedir):
    app = QApplication([])
    gui = Window()
    gui.show()
    controller = Controller("http://localhost", gui, mocker.MagicMock(), homedir, proxy=False)
    controller.qubes = False
    gui.setup(controller)
    gui.login_dialog.close()
    app.setActiveWindow(gui)
    dialog = ExportDialog(controller, "file_uuid", "file_name")
    dialog.show()

    yield dialog

    dialog.close()
    gui.close()
Пример #25
0
def test_Controller_on_sync_success_with_key_import_fail(
        homedir, config, mocker):
    """
    If there's a result to syncing, then update local storage.
    This is it to check that we can gracefully handle an import failure.
    Using the `config` fixture to ensure the config is written to disk.
    """
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    co.update_sources = mocker.MagicMock()
    co.api_runner = mocker.MagicMock()

    mock_source = mocker.MagicMock()
    mock_source.key = {
        'type': 'PGP',
        'public': PUB_KEY,
    }

    mock_sources = [mock_source]
    result_data = (mock_sources, 'submissions', 'replies')

    co.call_reset = mocker.MagicMock()
    mock_storage = mocker.patch('securedrop_client.logic.storage')
    mocker.patch.object(co.gpg, 'import_key', side_effect=CryptoError)
    co.on_sync_success(result_data)
    mock_storage.update_local_storage. \
        assert_called_once_with(mock_session, mock_sources, "submissions",
                                "replies",
                                os.path.join(homedir, 'data'))
    co.update_sources.assert_called_once_with()
Пример #26
0
def test_Controller_on_file_open(homedir, config, mocker):
    """
    If running on Qubes, a new QProcess with the expected command and args
    should be started.
    Using the `config` fixture to ensure the config is written to disk.
    """
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    co.proxy = True
    mock_submission = mocker.MagicMock()
    mock_submission.filename = '1-test.pdf'
    mock_subprocess = mocker.MagicMock()
    mock_process = mocker.MagicMock(return_value=mock_subprocess)
    mocker.patch('securedrop_client.logic.QProcess', mock_process)
    co.on_file_open(mock_submission)
    mock_process.assert_called_once_with(co)
    mock_subprocess.start.call_count == 1
Пример #27
0
def test_Controller_update_sources(homedir, config, mocker):
    """
    Ensure the UI displays a list of the available sources from local data
    store.
    Using the `config` fixture to ensure the config is written to disk.
    """
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    mock_storage = mocker.patch('securedrop_client.logic.storage')
    source_list = [
        factory.Source(last_updated=2),
        factory.Source(last_updated=1)
    ]
    mock_storage.get_local_sources.return_value = source_list
    co.update_sources()
    mock_storage.get_local_sources.assert_called_once_with(mock_session)
    mock_gui.show_sources.assert_called_once_with(source_list)
Пример #28
0
def test_Controller_on_reply_failure(homedir, mocker):
    '''
    Check that when the result is a failure, the client emits the correct signal.
    '''
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()

    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    co.api = mocker.Mock()
    journalist_uuid = 'abc123'
    co.api.token_journalist_uuid = journalist_uuid
    mock_reply_succeeded = mocker.patch.object(co, 'reply_succeeded')
    mock_reply_failed = mocker.patch.object(co, 'reply_failed')

    source_uuid = 'foo111'
    msg_uuid = 'bar222'
    current_object = (source_uuid, msg_uuid)
    co.on_reply_failure(Exception, current_object)
    mock_reply_failed.emit.assert_called_once_with(msg_uuid)
    assert not mock_reply_succeeded.emit.called
Пример #29
0
def test_Controller_on_file_download_Reply(homedir, config, mocker):
    """
    If the handler is passed a reply, check the download_reply
    function is the one called against the API.
    Using the `config` fixture to ensure the config is written to disk.
    """
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    source = factory.Source()
    journalist = db.User('Testy mcTestface')
    reply = db.Reply(uuid='reply-uuid',
                     journalist=journalist,
                     source=source,
                     filename='1-my-reply.gpg',
                     size=123)  # Not a sdclientapi.Submission
    co.call_api = mocker.MagicMock()
    co.api = mocker.MagicMock()
    reply_sdk_object = mocker.MagicMock()
    mock_reply = mocker.patch('sdclientapi.Reply')
    mock_reply.return_value = reply_sdk_object
    co.on_file_download(source, reply)
    co.call_api.assert_called_once_with(co.api.download_reply,
                                        co.on_file_download_success,
                                        co.on_file_download_failure,
                                        reply_sdk_object,
                                        co.data_dir,
                                        current_object=reply)
Пример #30
0
def test_Controller_on_file_download_Submission(homedir, config, mocker):
    """
    If the handler is passed a submission, check the download_submission
    function is the one called against the API.
    Using the `config` fixture to ensure the config is written to disk.
    """
    mock_gui = mocker.MagicMock()
    mock_session = mocker.MagicMock()
    co = Controller('http://localhost', mock_gui, mock_session, homedir)
    co.call_api = mocker.MagicMock()
    co.api = mocker.MagicMock()

    source = factory.Source()
    file_ = db.File(source=source,
                    uuid='uuid',
                    size=1234,
                    filename='1-myfile.doc.gpg',
                    download_url='http://myserver/myfile',
                    is_downloaded=False)

    submission_sdk_object = mocker.MagicMock()
    mock_submission = mocker.patch('sdclientapi.Submission')
    mock_submission.return_value = submission_sdk_object

    co.on_file_download(source, file_)
    co.call_api.assert_called_once_with(
        co.api.download_submission,
        co.on_file_download_success,
        co.on_file_download_failure,
        submission_sdk_object,
        co.data_dir,
        current_object=file_,
    )