示例#1
0
def test_create_duplicate_codename_logged_in_in_session(source_app):
    with source_app.test_client() as app:
        # Given a user who generated a codename in a browser tab
        resp = app.post(url_for("main.generate"), data=GENERATE_DATA)
        assert resp.status_code == 200
        first_tab_id, first_codename = list(session["codenames"].items())[0]

        # And then they opened a new browser tab to generate a second codename
        resp = app.post(url_for("main.generate"), data=GENERATE_DATA)
        assert resp.status_code == 200
        second_tab_id, second_codename = list(session["codenames"].items())[1]
        assert first_codename != second_codename

        # And the user then completed the account creation flow in the first tab
        resp = app.post(url_for("main.create"),
                        data={"tab_id": first_tab_id},
                        follow_redirects=True)
        assert resp.status_code == 200
        first_tab_account = SessionManager.get_logged_in_user(
            db_session=db.session)

        # When the user tries to complete the account creation flow again, in the second tab
        resp = app.post(url_for("main.create"),
                        data={"tab_id": second_tab_id},
                        follow_redirects=True)

        # Then the user is shown the "already logged in" message
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "You are already logged in." in text

        # And no new account was created
        second_tab_account = SessionManager.get_logged_in_user(
            db_session=db.session)
        assert second_tab_account.filesystem_id == first_tab_account.filesystem_id
示例#2
0
def test_create_duplicate_codename_logged_in_not_in_session(source_app):
    with patch.object(source_app.logger, "error") as logger:
        with source_app.test_client() as app:
            resp = app.post(url_for("main.generate"), data=GENERATE_DATA)
            assert resp.status_code == 200
            tab_id, codename = next(iter(session["codenames"].items()))

            # Create a source the first time
            resp = app.post(url_for("main.create"),
                            data={"tab_id": tab_id},
                            follow_redirects=True)
            assert resp.status_code == 200

        with source_app.test_client() as app:
            # Attempt to add the same source
            with app.session_transaction() as sess:
                sess["codenames"] = {tab_id: codename}
                sess["codenames_expire"] = datetime.utcnow() + timedelta(
                    hours=1)
            resp = app.post(url_for("main.create"),
                            data={"tab_id": tab_id},
                            follow_redirects=True)
            logger.assert_called_once()
            assert "Could not create a source" in logger.call_args[0][0]
            assert resp.status_code == 200
            assert not SessionManager.is_user_logged_in(db_session=db.session)
示例#3
0
def test_login_with_missing_reply_files(source_app, app_storage):
    """
    Test that source can log in when replies are present in database but missing
    from storage.
    """
    source, codename = utils.db_helper.init_source(app_storage)
    journalist, _ = utils.db_helper.init_journalist()
    replies = utils.db_helper.reply(app_storage, journalist, source, 1)
    assert len(replies) > 0
    # Delete the reply file
    reply_file_path = Path(
        app_storage.path(source.filesystem_id, replies[0].filename))
    reply_file_path.unlink()
    assert not reply_file_path.exists()

    with source_app.test_client() as app:
        resp = app.get(url_for("main.login"))
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Enter Codename" in text

        resp = app.post(url_for("main.login"),
                        data=dict(codename=codename),
                        follow_redirects=True)
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Submit Files" in text
        assert SessionManager.is_user_logged_in(db_session=db.session)
示例#4
0
    def generate() -> Union[str, werkzeug.Response]:
        if request.method == "POST":
            # Try to detect Tor2Web usage by looking to see if tor2web_check got mangled
            tor2web_check = request.form.get("tor2web_check")
            if tor2web_check is None:
                # Missing form field
                abort(403)
            elif tor2web_check != 'href="fake.onion"':
                return redirect(url_for("info.tor2web_warning"))

        if SessionManager.is_user_logged_in(db_session=db.session):
            flash_msg(
                "notification",
                None,
                gettext(
                    "You were redirected because you are already logged in. "
                    "If you want to create a new account, you should log out first."
                ),
            )
            return redirect(url_for(".lookup"))
        codename = PassphraseGenerator.get_default().generate_passphrase(
            preferred_language=g.localeinfo.language
        )

        # Generate a unique id for each browser tab and associate the codename with this id.
        # This will allow retrieval of the codename displayed in the tab from which the source has
        # clicked to proceed to /generate (ref. issue #4458)
        tab_id = urlsafe_b64encode(os.urandom(64)).decode()
        codenames = session.get("codenames", {})
        codenames[tab_id] = codename
        session["codenames"] = fit_codenames_into_cookie(codenames)
        session["codenames_expire"] = datetime.now(timezone.utc) + timedelta(
            minutes=config.SESSION_EXPIRATION_MINUTES
        )
        return render_template("generate.html", codename=codename, tab_id=tab_id)
示例#5
0
    def login() -> Union[str, werkzeug.Response]:
        form = LoginForm()
        if form.validate_on_submit():
            try:
                SessionManager.log_user_in(
                    db_session=db.session,
                    supplied_passphrase=DicewarePassphrase(request.form["codename"].strip()),
                )
            except InvalidPassphraseError:
                current_app.logger.info("Login failed for invalid codename")
                flash_msg("error", None, gettext("Sorry, that is not a recognized codename."))
            else:
                # Success: a valid passphrase was supplied
                return redirect(url_for(".lookup", from_login="******"))

        return render_template("login.html", form=form)
    def test_log_user_in(self, source_app, app_storage):
        # Given a source user
        passphrase = PassphraseGenerator.get_default().generate_passphrase()
        source_user = create_source_user(
            db_session=db.session,
            source_passphrase=passphrase,
            source_app_storage=app_storage,
        )

        with source_app.test_request_context():
            # When they log in, it succeeds
            SessionManager.log_user_in(db_session=db.session, supplied_passphrase=passphrase)

            # And the SessionManager returns them as the current user
            assert SessionManager.is_user_logged_in(db_session=db.session)
            logged_in_user = SessionManager.get_logged_in_user(db_session=db.session)
            assert logged_in_user.db_record_id == source_user.db_record_id
示例#7
0
    def create() -> werkzeug.Response:
        if SessionManager.is_user_logged_in(db_session=db.session):
            flash_msg(
                "notification",
                None,
                gettext(
                    "You are already logged in. Please verify your codename as it "
                    "may differ from the one displayed on the previous page."
                ),
            )
        else:
            # Ensure the codenames have not expired
            date_codenames_expire = session.get("codenames_expire")
            if not date_codenames_expire or datetime.now(timezone.utc) >= date_codenames_expire:
                return clear_session_and_redirect_to_logged_out_page(flask_session=session)

            tab_id = request.form["tab_id"]
            codename = session["codenames"][tab_id]
            del session["codenames"]

            try:
                current_app.logger.info("Creating new source user...")
                create_source_user(
                    db_session=db.session,
                    source_passphrase=codename,
                    source_app_storage=Storage.get_default(),
                )
            except (SourcePassphraseCollisionError, SourceDesignationCollisionError) as e:
                current_app.logger.error("Could not create a source: {}".format(e))
                flash_msg(
                    "error",
                    None,
                    gettext(
                        "There was a temporary problem creating your account. Please try again."
                    ),
                )
                return redirect(url_for(".index"))

            # All done - source user was successfully created
            current_app.logger.info("New source user created")
            session["new_user_codename"] = codename
            SessionManager.log_user_in(
                db_session=db.session, supplied_passphrase=DicewarePassphrase(codename)
            )

        return redirect(url_for(".lookup"))
示例#8
0
    def logout() -> Union[str, werkzeug.Response]:
        """
        If a user is logged in, show them a logout page that prompts them to
        click the New Identity button in Tor Browser to complete their session.
        Otherwise redirect to the main Source Interface page.
        """
        if SessionManager.is_user_logged_in(db_session=db.session):
            SessionManager.log_user_out()

            # Clear the session after we render the message so it's localized
            # If a user specified a locale, save it and restore it
            session.clear()
            session["locale"] = g.localeinfo.id

            return render_template("logout.html")
        else:
            return redirect(url_for(".index"))
示例#9
0
def test_login_and_logout(source_app):
    with source_app.test_client() as app:
        resp = app.get(url_for("main.login"))
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Enter Codename" in text

        codename = new_codename(app, session)
        resp = app.post(url_for("main.login"),
                        data=dict(codename=codename),
                        follow_redirects=True)
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Submit Files" in text
        assert SessionManager.is_user_logged_in(db_session=db.session)

    with source_app.test_client() as app:
        resp = app.post(url_for("main.login"),
                        data=dict(codename="invalid"),
                        follow_redirects=True)
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Sorry, that is not a recognized codename." in text
        assert not SessionManager.is_user_logged_in(db_session=db.session)

    with source_app.test_client() as app:
        resp = app.post(url_for("main.login"),
                        data=dict(codename=codename),
                        follow_redirects=True)
        assert resp.status_code == 200
        assert SessionManager.is_user_logged_in(db_session=db.session)

        resp = app.post(url_for("main.login"),
                        data=dict(codename=codename),
                        follow_redirects=True)
        assert resp.status_code == 200
        assert SessionManager.is_user_logged_in(db_session=db.session)

        resp = app.get(url_for("main.logout"), follow_redirects=True)
        assert not SessionManager.is_user_logged_in(db_session=db.session)
        text = resp.data.decode("utf-8")

        # This is part of the logout page message instructing users
        # to click the 'New Identity' icon
        assert "This will clear your Tor Browser activity data" in text
示例#10
0
def test_source_can_only_delete_own_replies(source_app, app_storage):
    """This test checks for a bug an authenticated source A could delete
    replies send to source B by "guessing" the filename.
    """
    source0, codename0 = utils.db_helper.init_source(app_storage)
    source1, codename1 = utils.db_helper.init_source(app_storage)
    journalist, _ = utils.db_helper.init_journalist()
    replies = utils.db_helper.reply(app_storage, journalist, source0, 1)
    filename = replies[0].filename
    confirmation_msg = "Reply deleted"

    with source_app.test_client() as app:
        resp = app.post(url_for("main.login"),
                        data={"codename": codename1},
                        follow_redirects=True)
        assert resp.status_code == 200
        assert SessionManager.get_logged_in_user(
            db_session=db.session).db_record_id == source1.id

        resp = app.post(url_for("main.delete"),
                        data={"reply_filename": filename},
                        follow_redirects=True)
        assert resp.status_code == 404
        assert confirmation_msg not in resp.data.decode("utf-8")

    reply = Reply.query.filter_by(filename=filename).one()
    assert not reply.deleted_by_source

    with source_app.test_client() as app:
        resp = app.post(url_for("main.login"),
                        data={"codename": codename0},
                        follow_redirects=True)
        assert resp.status_code == 200
        assert SessionManager.get_logged_in_user(
            db_session=db.session).db_record_id == source0.id

        resp = app.post(url_for("main.delete"),
                        data={"reply_filename": filename},
                        follow_redirects=True)
        assert resp.status_code == 200
        assert confirmation_msg in resp.data.decode("utf-8")

    reply = Reply.query.filter_by(filename=filename).one()
    assert reply.deleted_by_source
示例#11
0
def test_source_is_deleted_while_logged_in(source_app):
    """If a source is deleted by a journalist when they are logged in,
    a NoResultFound will occur. The source should be redirected to the
    index when this happens, and a warning logged."""
    with source_app.test_client() as app:
        codename = new_codename(app, session)
        app.post("login", data=dict(codename=codename), follow_redirects=True)

        # Now that the source is logged in, the journalist deletes the source
        source_user = SessionManager.get_logged_in_user(db_session=db.session)
        delete_collection(source_user.filesystem_id)

        # Source attempts to continue to navigate
        resp = app.get(url_for("main.lookup"), follow_redirects=True)
        assert resp.status_code == 200
        assert not SessionManager.is_user_logged_in(db_session=db.session)
        text = resp.data.decode("utf-8")
        assert "First submission" in text
        assert not SessionManager.is_user_logged_in(db_session=db.session)
    def test_get_logged_in_user_but_user_deleted(self, source_app, app_storage):
        # Given a source user
        passphrase = PassphraseGenerator.get_default().generate_passphrase()
        source_user = create_source_user(
            db_session=db.session,
            source_passphrase=passphrase,
            source_app_storage=app_storage,
        )

        with source_app.test_request_context():
            # Who previously logged in
            SessionManager.log_user_in(db_session=db.session, supplied_passphrase=passphrase)
            # But since then their account was deleted
            source_in_db = source_user.get_db_record()
            source_in_db.deleted_at = datetime.utcnow()
            db.session.commit()

            # When querying the current user from the SessionManager, it fails with the right error
            with pytest.raises(UserHasBeenDeleted):
                SessionManager.get_logged_in_user(db_session=db.session)
示例#13
0
    def decorated_function(*args: Any, **kwargs: Any) -> Any:
        try:
            logged_in_source = SessionManager.get_logged_in_user(
                db_session=db.session)

        except (UserSessionExpired, UserHasBeenDeleted):
            return clear_session_and_redirect_to_logged_out_page(
                flask_session=session)

        except UserNotLoggedIn:
            return redirect(url_for("main.login"))

        return f(*args, **kwargs, logged_in_source=logged_in_source)
示例#14
0
    def login_test(app, codename):
        resp = app.get(url_for("main.login"))
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Enter Codename" in text

        resp = app.post(url_for("main.login"),
                        data=dict(codename=codename),
                        follow_redirects=True)
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Submit Files" in text
        assert SessionManager.is_user_logged_in(db_session=db.session)
    def test_get_logged_in_user_but_session_expired(self, source_app, app_storage):
        # Given a source user
        passphrase = PassphraseGenerator.get_default().generate_passphrase()
        create_source_user(
            db_session=db.session,
            source_passphrase=passphrase,
            source_app_storage=app_storage,
        )

        with source_app.test_request_context():
            # Who previously logged in
            SessionManager.log_user_in(db_session=db.session, supplied_passphrase=passphrase)

            # But we're now 6 hours later hence their session expired
            with mock.patch("source_app.session_manager.datetime") as mock_datetime:
                six_hours_later = datetime.now(timezone.utc) + timedelta(hours=6)
                mock_datetime.now.return_value = six_hours_later

                # When querying the current user from the SessionManager
                # it fails with the right error
                with pytest.raises(UserSessionExpired):
                    SessionManager.get_logged_in_user(db_session=db.session)
示例#16
0
def test_create_new_source(source_app):
    with source_app.test_client() as app:
        resp = app.post(url_for("main.generate"), data=GENERATE_DATA)
        assert resp.status_code == 200
        tab_id = next(iter(session["codenames"].keys()))
        resp = app.post(url_for("main.create"),
                        data={"tab_id": tab_id},
                        follow_redirects=True)
        assert SessionManager.is_user_logged_in(db_session=db.session)
        # should be redirected to /lookup
        text = resp.data.decode("utf-8")
        assert "Submit Files" in text
        assert "codenames" not in session
示例#17
0
def test_submit_codename_second_login(source_app):
    """
    Test codename submissions *not* prevented on second session
    """
    with source_app.test_client() as app:
        InstanceConfig.get_default().update_submission_prefs(
            allow_uploads=True, min_length=0, reject_codenames=True)
        codename = new_codename(app, session)
        resp = app.post(
            url_for("main.submit"),
            data=dict(msg=codename, fh=(StringIO(""), "")),
            follow_redirects=True,
        )
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Please do not submit your codename!" in text

        resp = app.get(url_for("main.logout"), follow_redirects=True)
        assert not SessionManager.is_user_logged_in(db_session=db.session)
        text = resp.data.decode("utf-8")
        assert "This will clear your Tor Browser activity data" in text

        resp = app.post(url_for("main.login"),
                        data=dict(codename=codename),
                        follow_redirects=True)
        assert resp.status_code == 200
        assert SessionManager.is_user_logged_in(db_session=db.session)

        resp = app.post(
            url_for("main.submit"),
            data=dict(msg=codename, fh=(StringIO(""), "")),
            follow_redirects=True,
        )
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Thank you for sending this information" in text
    def test_log_user_out(self, source_app, app_storage):
        # Given a source user
        passphrase = PassphraseGenerator.get_default().generate_passphrase()
        create_source_user(
            db_session=db.session,
            source_passphrase=passphrase,
            source_app_storage=app_storage,
        )

        with source_app.test_request_context():
            # Who previously logged in
            SessionManager.log_user_in(db_session=db.session, supplied_passphrase=passphrase)

            # When they log out, it succeeds
            SessionManager.log_user_out()

            # And the SessionManager no longer returns a current user
            assert not SessionManager.is_user_logged_in(db_session=db.session)
            with pytest.raises(UserNotLoggedIn):
                SessionManager.get_logged_in_user(db_session=db.session)
示例#19
0
def test_normalize_timestamps(source_app, app_storage):
    """
    Check function of source_app.utils.normalize_timestamps.

    All submissions for a source should have the same timestamp. Any
    existing submissions' files that did not exist at the time of a
    new submission should not be created by normalize_timestamps.
    """
    with source_app.test_client() as app:
        # create a source
        source, codename = utils.db_helper.init_source(app_storage)

        # create one submission
        first_submission = submit(app_storage, source, 1)[0]

        # delete the submission's file from the store
        first_submission_path = Path(
            app_storage.path(source.filesystem_id, first_submission.filename))
        first_submission_path.unlink()
        assert not first_submission_path.exists()

        # log in as the source
        resp = app.post(url_for("main.login"),
                        data=dict(codename=codename),
                        follow_redirects=True)
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Submit Files" in text
        assert SessionManager.is_user_logged_in(db_session=db.session)

        # submit another message
        resp = _dummy_submission(app)
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Thanks! We received your message" in text

        # sleep to ensure timestamps would differ
        time.sleep(1)

        # submit another message
        resp = _dummy_submission(app)
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Thanks! We received your message" in text

        # only two of the source's three submissions should have files in the store
        assert 3 == len(source.submissions)
        submission_paths = [
            Path(app_storage.path(source.filesystem_id, s.filename))
            for s in source.submissions
        ]
        extant_paths = [p for p in submission_paths if p.exists()]
        assert 2 == len(extant_paths)

        # verify that the deleted file has not been recreated
        assert not first_submission_path.exists()
        assert first_submission_path not in extant_paths

        # and the timestamps of all existing files should match exactly
        assert extant_paths[0].stat().st_atime_ns == extant_paths[1].stat(
        ).st_atime_ns
        assert extant_paths[0].stat().st_ctime_ns == extant_paths[1].stat(
        ).st_ctime_ns
        assert extant_paths[0].stat().st_mtime_ns == extant_paths[1].stat(
        ).st_mtime_ns
示例#20
0
def _helper_test_reply(journalist_app,
                       source_app,
                       config,
                       test_journo,
                       test_reply,
                       expected_success=True):
    test_msg = "This is a test message."

    with source_app.test_client() as app:
        app.post("/generate", data=GENERATE_DATA)
        tab_id, codename = next(iter(session["codenames"].items()))
        app.post("/create", data={"tab_id": tab_id}, follow_redirects=True)
        # redirected to submission form
        resp = app.post(
            "/submit",
            data=dict(
                msg=test_msg,
                fh=(BytesIO(b""), ""),
            ),
            follow_redirects=True,
        )
        assert resp.status_code == 200
        source_user = SessionManager.get_logged_in_user(db_session=db.session)
        filesystem_id = source_user.filesystem_id
        app.get("/logout")

    with journalist_app.test_client() as app:
        _login_user(app, test_journo)
        resp = app.get("/")
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Sources" in text
        soup = BeautifulSoup(resp.data, "html.parser")
        col_url = soup.select(
            "table#collections tr.source > th.designation a")[0]["href"]

        resp = app.get(col_url)
        assert resp.status_code == 200

    assert EncryptionManager.get_default().get_source_key_fingerprint(
        filesystem_id)

    # Create 2 replies to test deleting on journalist and source interface
    with journalist_app.test_client() as app:
        _login_user(app, test_journo)
        for i in range(2):
            resp = app.post(
                "/reply",
                data=dict(filesystem_id=filesystem_id, message=test_reply),
                follow_redirects=True,
            )
            assert resp.status_code == 200

        if not expected_success:
            pass
        else:
            text = resp.data.decode("utf-8")
            assert "The source will receive your reply" in text

        resp = app.get(col_url)
        text = resp.data.decode("utf-8")
        assert "reply-" in text

    soup = BeautifulSoup(text, "html.parser")

    # Download the reply and verify that it can be decrypted with the
    # journalist's key as well as the source's reply key
    filesystem_id = soup.select('input[name="filesystem_id"]')[0]["value"]
    checkbox_values = [
        soup.select('input[name="doc_names_selected"]')[1]["value"]
    ]
    resp = app.post(
        "/bulk",
        data=dict(filesystem_id=filesystem_id,
                  action="download",
                  doc_names_selected=checkbox_values),
        follow_redirects=True,
    )
    assert resp.status_code == 200

    zf = zipfile.ZipFile(BytesIO(resp.data), "r")
    data = zf.read(zf.namelist()[0])
    _can_decrypt_with_journalist_secret_key(data)
    _can_decrypt_with_source_secret_key(data, source_user.gpg_secret)

    # Test deleting reply on the journalist interface
    last_reply_number = len(
        soup.select('input[name="doc_names_selected"]')) - 1
    _helper_filenames_delete(app, soup, last_reply_number)

    with source_app.test_client() as app:
        resp = app.post("/login",
                        data=dict(codename=codename),
                        follow_redirects=True)
        assert resp.status_code == 200
        resp = app.get("/lookup")
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")

        if not expected_success:
            # there should be no reply
            assert "You have received a reply." not in text
        else:
            assert "You have received a reply. To protect your identity" in text
            assert test_reply in text, text
            soup = BeautifulSoup(text, "html.parser")
            msgid = soup.select(
                'form > input[name="reply_filename"]')[0]["value"]
            resp = app.post(
                "/delete",
                data=dict(filesystem_id=filesystem_id, reply_filename=msgid),
                follow_redirects=True,
            )
            assert resp.status_code == 200
            text = resp.data.decode("utf-8")
            assert "Reply deleted" in text

        app.get("/logout")
示例#21
0
def test_submit_message(journalist_app, source_app, test_journo, app_storage):
    """When a source creates an account, test that a new entry appears
    in the journalist interface"""
    test_msg = "This is a test message."

    with source_app.test_client() as app:
        app.post("/generate", data=GENERATE_DATA)
        tab_id = next(iter(session["codenames"].keys()))
        app.post("/create", data={"tab_id": tab_id}, follow_redirects=True)
        source_user = SessionManager.get_logged_in_user(db_session=db.session)
        filesystem_id = source_user.filesystem_id

        # redirected to submission form
        resp = app.post(
            "/submit",
            data=dict(
                msg=test_msg,
                fh=(BytesIO(b""), ""),
            ),
            follow_redirects=True,
        )
        assert resp.status_code == 200
        app.get("/logout")

    # Request the Journalist Interface index
    with journalist_app.test_client() as app:
        _login_user(app, test_journo)
        resp = app.get("/")
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "Sources" in text
        soup = BeautifulSoup(text, "html.parser")

        # The source should have a "download unread" link that
        # says "1 unread"
        col = soup.select("table#collections tr.source")[0]
        unread_span = col.select("td.unread a")[0]
        assert "1 unread" in unread_span.get_text()

        col_url = soup.select("table#collections th.designation a")[0]["href"]
        resp = app.get(col_url)
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        soup = BeautifulSoup(text, "html.parser")
        submission_url = soup.select(
            "table#submissions th.filename a")[0]["href"]
        assert "-msg" in submission_url
        size = soup.select("table#submissions td.info")[0]
        assert re.compile(r"\d+ bytes").match(size["title"])

        resp = app.get(submission_url)
        assert resp.status_code == 200

        encryption_mgr = EncryptionManager.get_default()
        with import_journalist_private_key(encryption_mgr):
            decryption_result = encryption_mgr._gpg.decrypt(resp.data)
        assert decryption_result.ok
        assert decryption_result.data.decode("utf-8") == test_msg

        # delete submission
        resp = app.get(col_url)
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        soup = BeautifulSoup(text, "html.parser")
        doc_name = soup.select(
            'table#submissions > tr.submission > td.status input[name="doc_names_selected"]'
        )[0]["value"]
        resp = app.post(
            "/bulk",
            data=dict(
                action="delete",
                filesystem_id=filesystem_id,
                doc_names_selected=doc_name,
            ),
            follow_redirects=True,
        )
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        soup = BeautifulSoup(text, "html.parser")
        assert "The item has been deleted." in text

        # confirm that submission deleted and absent in list of submissions
        resp = app.get(col_url)
        assert resp.status_code == 200
        text = resp.data.decode("utf-8")
        assert "No submissions to display." in text

        # the file should be deleted from the filesystem
        # since file deletion is handled by a polling worker, this test
        # needs to wait for the worker to get the job and execute it
        def assertion():
            assert not (os.path.exists(
                app_storage.path(filesystem_id, doc_name)))

        utils.asynchronous.wait_for_assertion(assertion)