def lookup(): replies = [] for fn in os.listdir(g.loc): if fn.startswith('reply-'): try: msg = crypto_util.decrypt(g.sid, g.codename, file(store.path(g.sid, fn)).read()).decode("utf-8") except UnicodeDecodeError: app.logger.error("Could not decode reply %s" % fn) else: date = str(datetime.fromtimestamp( os.stat(store.path(g.sid, fn)).st_mtime)) replies.append(dict(id=fn, date=date, msg=msg)) def async_genkey(sid, codename): with app.app_context(): background.execute(lambda: crypto_util.genkeypair(sid, codename)) # Generate a keypair to encrypt replies from the journalist # Only do this if the journalist has flagged the source as one # that they would like to reply to. (Issue #140.) if not crypto_util.getkey(g.sid) and g.source.flagged: async_genkey(g.sid, g.codename) return render_template('lookup.html', codename=g.codename, msgs=replies, flagged=g.source.flagged, haskey=crypto_util.getkey(g.sid))
def lookup(): msgs = [] flagged = False for fn in os.listdir(g.loc): if fn == '_FLAG': flagged = True continue if fn.startswith('reply-'): msgs.append(dict( id=fn, date=str( datetime.fromtimestamp( os.stat(store.path(g.sid, fn)).st_mtime)), msg=crypto_util.decrypt( g.sid, g.codename, file(store.path(g.sid, fn)).read()) )) if flagged: session['flagged'] = True def async_genkey(sid, codename): with app.app_context(): background.execute(lambda: crypto_util.genkeypair(sid, codename)) # Generate a keypair to encrypt replies from the journalist # Only do this if the journalist has flagged the source as one # that they would like to reply to. (Issue #140.) if not crypto_util.getkey(g.sid) and flagged: async_genkey(g.sid, g.codename) return render_template( 'lookup.html', codename=g.codename, msgs=msgs, flagged=flagged, haskey=crypto_util.getkey(g.sid))
def lookup(): replies = [] for reply in g.source.replies: reply_path = store.path(g.filesystem_id, reply.filename) try: reply.decrypted = crypto_util.decrypt( g.codename, open(reply_path).read()).decode('utf-8') except UnicodeDecodeError: app.logger.error("Could not decode reply %s" % reply.filename) else: reply.date = datetime.utcfromtimestamp( os.stat(reply_path).st_mtime) replies.append(reply) # Sort the replies by date replies.sort(key=operator.attrgetter('date'), reverse=True) # Generate a keypair to encrypt replies from the journalist # Only do this if the journalist has flagged the source as one # that they would like to reply to. (Issue #140.) if not crypto_util.getkey(g.filesystem_id) and g.source.flagged: async_genkey(g.filesystem_id, g.codename) return render_template('lookup.html', codename=g.codename, replies=replies, flagged=g.source.flagged, haskey=crypto_util.getkey(g.filesystem_id))
def test_delete_source_deletes_source_key(self): """Verify that when a source is deleted, the PGP key that corresponds to them is also deleted.""" self._delete_collection_setup() # Source key exists source_key = crypto_util.getkey(self.source.filesystem_id) self.assertNotEqual(source_key, None) journalist.delete_collection(self.source.filesystem_id) # Source key no longer exists source_key = crypto_util.getkey(self.source.filesystem_id) self.assertEqual(source_key, None)
def reply(journalist, source, num_replies): """Generates and submits *num_replies* replies to *source* from *journalist*. Returns reply objects as a list. :param db.Journalist journalist: The journalist to write the reply from. :param db.Source source: The source to send the reply to. :param int num_replies: Number of random-data replies to make. :returns: A list of the :class:`db.Reply`s submitted. """ assert num_replies >= 1 replies = [] for _ in range(num_replies): source.interaction_count += 1 fname = "{}-{}-reply.gpg".format(source.interaction_count, source.journalist_filename) crypto_util.encrypt(str(os.urandom(1)), [ crypto_util.getkey(source.filesystem_id), config.JOURNALIST_KEY ], store.path(source.filesystem_id, fname)) reply = db.Reply(journalist, source, fname) replies.append(reply) db.db_session.add(reply) db.db_session.commit() return replies
def col(sid): source = get_source(sid) docs = get_docs(sid) haskey = crypto_util.getkey(sid) return render_template("col.html", sid=sid, codename=source.journalist_designation, docs=docs, haskey=haskey, flagged=source.flagged)
def reply(journalist, source, num_replies): """Generates and submits *num_replies* replies to *source* from *journalist*. Returns reply objects as a list. :param db.Journalist journalist: The journalist to write the reply from. :param db.Source source: The source to send the reply to. :param int num_replies: Number of random-data replies to make. :returns: A list of the :class:`db.Reply`s submitted. """ assert num_replies >= 1 replies = [] for _ in range(num_replies): source.interaction_count += 1 fname = "{}-{}-reply.gpg".format(source.interaction_count, source.journalist_filename) crypto_util.encrypt( str(os.urandom(1)), [crypto_util.getkey(source.filesystem_id), config.JOURNALIST_KEY], store.path(source.filesystem_id, fname)) reply = db.Reply(journalist, source, fname) replies.append(reply) db.db_session.add(reply) db.db_session.commit() return replies
def col(filesystem_id): form = ReplyForm() source = get_source(filesystem_id) source.has_key = crypto_util.getkey(filesystem_id) return render_template("col.html", filesystem_id=filesystem_id, source=source, form=form)
def reply(): sid, msg_candidate = request.form["sid"], request.form["msg"] try: msg = msg_candidate.decode() except (UnicodeDecodeError, UnicodeEncodeError): flash("You have entered text that we could not parse. Please try again.", "notification") return render_template("col.html", sid=sid, codename=db.display_id(sid, db.sqlalchemy_handle())) crypto_util.encrypt(crypto_util.getkey(sid), msg, output=store.path(sid, "reply-%s.gpg" % uuid.uuid4())) return render_template("reply.html", sid=sid, codename=db.display_id(sid, db.sqlalchemy_handle()))
def col(sid): docs, flagged = get_docs(sid) haskey = crypto_util.getkey(sid) return render_template("col.html", sid=sid, codename=db.display_id(sid, db.sqlalchemy_handle()), docs=docs, haskey=haskey, flagged=flagged)
def test_genkeypair(self): codename = crypto_util.genrandomid() filesystem_id = crypto_util.hash_codename(codename) journalist_filename = crypto_util.display_id() source = db.Source(filesystem_id, journalist_filename) db.db_session.add(source) db.db_session.commit() crypto_util.genkeypair(source.filesystem_id, codename) self.assertIsNotNone(crypto_util.getkey(filesystem_id))
def col(sid): docs, flagged = get_docs(sid) haskey = crypto_util.getkey(sid) return render_template( "col.html", sid=sid, codename=db.display_id(sid, db.sqlalchemy_handle()), docs=docs, haskey=haskey, flagged=flagged, )
def test_encrypt_success(self): source, _ = utils.db_helper.init_source() message = str(os.urandom(1)) ciphertext = crypto_util.encrypt( message, [crypto_util.getkey(source.filesystem_id), config.JOURNALIST_KEY], store.path(source.filesystem_id, 'somefile.gpg')) self.assertIsInstance(ciphertext, str) self.assertNotEqual(ciphertext, message) self.assertGreater(len(ciphertext), 0)
def lookup(): msgs = [] flagged = False for fn in os.listdir(g.loc): # TODO: make 'flag' a db column, so we can replace this with a db # lookup in the future if fn == '_FLAG': flagged = True continue if fn.startswith('reply-'): msg_candidate = crypto_util.decrypt( g.sid, g.codename, file(store.path(g.sid, fn)).read()) try: msgs.append( dict(id=fn, date=str( datetime.fromtimestamp( os.stat(store.path(g.sid, fn)).st_mtime)), msg=msg_candidate.decode())) except UnicodeDecodeError: # todo: we should have logging here! pass if flagged: session['flagged'] = True def async_genkey(sid, codename): with app.app_context(): background.execute(lambda: crypto_util.genkeypair(sid, codename)) # Generate a keypair to encrypt replies from the journalist # Only do this if the journalist has flagged the source as one # that they would like to reply to. (Issue #140.) if not crypto_util.getkey(g.sid) and flagged: async_genkey(g.sid, g.codename) return render_template('lookup.html', codename=g.codename, msgs=msgs, flagged=flagged, haskey=crypto_util.getkey(g.sid))
def reply(): g.source.interaction_count += 1 filename = "{0}-{1}-reply.gpg".format(g.source.interaction_count, g.source.journalist_filename) crypto_util.encrypt( request.form["msg"], [crypto_util.getkey(g.sid), config.JOURNALIST_KEY], output=store.path(g.sid, filename) ) reply = Reply(g.user, g.source, filename) db_session.add(reply) db_session.commit() flash("Thanks! Your reply has been stored.", "notification") return redirect(url_for("col", sid=g.sid))
def test_encrypt_fingerprints_not_a_list_or_tuple(self): """If passed a single fingerprint as a string, encrypt should correctly place that string in a list, and encryption/ decryption should work as intended.""" source, codename = utils.db_helper.init_source() message = str(os.urandom(1)) ciphertext = crypto_util.encrypt( message, crypto_util.getkey(source.filesystem_id), store.path(source.filesystem_id, 'somefile.gpg')) plaintext = crypto_util.decrypt(codename, ciphertext) self.assertEqual(message, plaintext)
def test_encrypt_without_output(self): """We simply do not specify the option output keyword argument to crypto_util.encrypt() here in order to confirm encryption works when it defaults to `None`. """ source, codename = utils.db_helper.init_source() message = str(os.urandom(1)) ciphertext = crypto_util.encrypt( message, [crypto_util.getkey(source.filesystem_id), config.JOURNALIST_KEY]) plaintext = crypto_util.decrypt(codename, ciphertext) self.assertEqual(message, plaintext)
def reply(): g.source.interaction_count += 1 filename = "{0}-{1}-reply.gpg".format(g.source.interaction_count, g.source.journalist_filename) crypto_util.encrypt(request.form['msg'], [crypto_util.getkey(g.sid), config.JOURNALIST_KEY], output=store.path(g.sid, filename)) reply = Reply(g.user, g.source, filename) db_session.add(reply) db_session.commit() flash("Thanks! Your reply has been stored.", "notification") return redirect(url_for('col', sid=g.sid))
def lookup(): msgs = [] flagged = False for fn in os.listdir(g.loc): # TODO: make 'flag' a db column, so we can replace this with a db # lookup in the future if fn == '_FLAG': flagged = True continue if fn.startswith('reply-'): msg_candidate = crypto_util.decrypt( g.sid, g.codename, file(store.path(g.sid, fn)).read()) try: msgs.append(dict( id=fn, date=str( datetime.fromtimestamp( os.stat(store.path(g.sid, fn)).st_mtime)), msg=msg_candidate.decode())) except UnicodeDecodeError: # todo: we should have logging here! pass if flagged: session['flagged'] = True def async_genkey(sid, codename): with app.app_context(): background.execute(lambda: crypto_util.genkeypair(sid, codename)) # Generate a keypair to encrypt replies from the journalist # Only do this if the journalist has flagged the source as one # that they would like to reply to. (Issue #140.) if not crypto_util.getkey(g.sid) and flagged: async_genkey(g.sid, g.codename) return render_template( 'lookup.html', codename=g.codename, msgs=msgs, flagged=flagged, haskey=crypto_util.getkey(g.sid))
def reply(): """Attempt to send a Reply from a Journalist to a Source. Empty messages are rejected, and an informative error message is flashed on the client. In the case of unexpected errors involving database transactions (potentially caused by racing request threads that modify the same the database object) logging is done in such a way so as not to write potentially sensitive information to disk, and a generic error message is flashed on the client. Returns: flask.Response: The user is redirected to the same Source collection view, regardless if the Reply is created successfully. """ form = ReplyForm() if not form.validate_on_submit(): for error in form.message.errors: flash(error, "error") return redirect(url_for('col.col', filesystem_id=g.filesystem_id)) g.source.interaction_count += 1 filename = "{0}-{1}-reply.gpg".format(g.source.interaction_count, g.source.journalist_filename) crypto_util.encrypt(form.message.data, [crypto_util.getkey(g.filesystem_id), config.JOURNALIST_KEY], output=store.path(g.filesystem_id, filename)) reply = Reply(g.user, g.source, filename) try: db_session.add(reply) db_session.commit() except Exception as exc: flash(gettext( "An unexpected error occurred! Please " "inform your administrator."), "error") # We take a cautious approach to logging here because we're dealing # with responses to sources. It's possible the exception message # could contain information we don't want to write to disk. current_app.logger.error( "Reply from '{}' (ID {}) failed: {}!".format(g.user.username, g.user.id, exc.__class__)) else: flash(gettext("Thanks. Your reply has been stored."), "notification") finally: return redirect(url_for('col.col', filesystem_id=g.filesystem_id))
def reply(): """Attempt to send a Reply from a Journalist to a Source. Empty messages are rejected, and an informative error message is flashed on the client. In the case of unexpected errors involving database transactions (potentially caused by racing request threads that modify the same the database object) logging is done in such a way so as not to write potentially sensitive information to disk, and a generic error message is flashed on the client. Returns: flask.Response: The user is redirected to the same Source collection view, regardless if the Reply is created successfully. """ form = ReplyForm() if not form.validate_on_submit(): for error in form.message.errors: flash(error, "error") return redirect(url_for('col.col', filesystem_id=g.filesystem_id)) g.source.interaction_count += 1 filename = "{0}-{1}-reply.gpg".format(g.source.interaction_count, g.source.journalist_filename) crypto_util.encrypt( form.message.data, [crypto_util.getkey(g.filesystem_id), config.JOURNALIST_KEY], output=store.path(g.filesystem_id, filename)) reply = Reply(g.user, g.source, filename) try: db_session.add(reply) db_session.commit() except Exception as exc: flash( gettext("An unexpected error occurred! Please " "inform your administrator."), "error") # We take a cautious approach to logging here because we're dealing # with responses to sources. It's possible the exception message # could contain information we don't want to write to disk. current_app.logger.error( "Reply from '{}' (ID {}) failed: {}!".format( g.user.username, g.user.id, exc.__class__)) else: flash(gettext("Thanks. Your reply has been stored."), "notification") finally: return redirect(url_for('col.col', filesystem_id=g.filesystem_id))
def reply(): sid, msg_candidate = request.form['sid'], request.form['msg'] try: msg = msg_candidate.decode() except (UnicodeDecodeError, UnicodeEncodeError): flash( "You have entered text that we could not parse. Please try again.", "notification") return render_template('col.html', sid=sid, codename=db.display_id(sid, db.sqlalchemy_handle())) crypto_util.encrypt(crypto_util.getkey(sid), msg, output=store.path(sid, 'reply-%s.gpg' % uuid.uuid4())) return render_template('reply.html', sid=sid, codename=db.display_id(sid, db.sqlalchemy_handle()))
def test_basic_encrypt_then_decrypt_multiple_recipients(self): source, codename = utils.db_helper.init_source() message = str(os.urandom(1)) ciphertext = crypto_util.encrypt( message, [crypto_util.getkey(source.filesystem_id), config.JOURNALIST_KEY], store.path(source.filesystem_id, 'somefile.gpg')) plaintext = crypto_util.decrypt(codename, ciphertext) self.assertEqual(message, plaintext) # Since there's no way to specify which key to use for # decryption to python-gnupg, we delete the `source`'s key and # ensure we can decrypt with the `config.JOURNALIST_KEY`. crypto_util.delete_reply_keypair(source.filesystem_id) plaintext_ = crypto_util.gpg.decrypt(ciphertext).data self.assertEqual(message, plaintext_)
def test_encrypt_binary_stream(self): """Generally, we pass unicode strings (the type form data is returned as) as plaintext to crypto_util.encrypt(). These have to be converted to "binary stream" types (such as `file`) before we can actually call gnupg.GPG.encrypt() on them. This is done in crypto_util.encrypt() with an `if` branch that uses `gnupg._util._is_stream(plaintext)` as the predicate, and calls `gnupg._util._make_binary_stream(plaintext)` if necessary. This test ensures our encrypt function works even if we provide inputs such that this `if` branch is skipped (i.e., the object passed for `plaintext` is one such that `gnupg._util._is_stream(plaintext)` returns `True`). """ source, codename = utils.db_helper.init_source() with open(os.path.realpath(__file__)) as fh: ciphertext = crypto_util.encrypt( fh, [crypto_util.getkey(source.filesystem_id), config.JOURNALIST_KEY], store.path(source.filesystem_id, 'somefile.gpg')) plaintext = crypto_util.decrypt(codename, ciphertext) with open(os.path.realpath(__file__)) as fh: self.assertEqual(fh.read(), plaintext)
def test_delete_reply_keypair(self): source, _ = utils.db_helper.init_source() crypto_util.delete_reply_keypair(source.filesystem_id) self.assertIsNone(crypto_util.getkey(source.filesystem_id))
def key_available(filesystem_id): assert crypto_util.getkey(filesystem_id)
def _block_on_reply_keypair_gen(codename): sid = crypto_util.shash(codename) while not crypto_util.getkey(sid): sleep(0.1)
def reply(): msg = request.form['msg'] crypto_util.encrypt(crypto_util.getkey(g.sid), msg, output= store.path(g.sid, 'reply-%s.gpg' % uuid.uuid4())) return render_template('reply.html', sid=g.sid, codename=g.source.journalist_designation)
def helper_test_reply(self, test_reply, expected_success=True): test_msg = "This is a test message." with self.source_app as source_app: resp = source_app.get('/generate') resp = source_app.post('/create', follow_redirects=True) codename = session['codename'] filesystem_id = g.filesystem_id # redirected to submission form resp = source_app.post('/submit', data=dict( msg=test_msg, fh=(StringIO(''), ''), ), follow_redirects=True) self.assertEqual(resp.status_code, 200) self.assertFalse(g.source.flagged) source_app.get('/logout') resp = self.journalist_app.get('/') self.assertEqual(resp.status_code, 200) self.assertIn("Sources", resp.data) soup = BeautifulSoup(resp.data, 'html.parser') col_url = soup.select('ul#cols > li a')[0]['href'] resp = self.journalist_app.get(col_url) self.assertEqual(resp.status_code, 200) with self.source_app as source_app: resp = source_app.post('/login', data=dict( codename=codename), follow_redirects=True) self.assertEqual(resp.status_code, 200) self.assertFalse(g.source.flagged) source_app.get('/logout') with self.journalist_app as journalist_app: resp = journalist_app.post('/flag', data=dict( filesystem_id=filesystem_id)) self.assertEqual(resp.status_code, 200) with self.source_app as source_app: resp = source_app.post('/login', data=dict( codename=codename), follow_redirects=True) self.assertEqual(resp.status_code, 200) self.assertTrue(g.source.flagged) source_app.get('/lookup') self.assertTrue(g.source.flagged) source_app.get('/logout') # Block up to 15s for the reply keypair, so we can test sending a reply utils.async.wait_for_assertion( lambda: self.assertNotEqual(crypto_util.getkey(filesystem_id), None), 15) # Create 2 replies to test deleting on journalist and source interface for i in range(2): resp = self.journalist_app.post('/reply', data=dict( filesystem_id=filesystem_id, message=test_reply ), follow_redirects=True) self.assertEqual(resp.status_code, 200) if not expected_success: pass else: self.assertIn("Thanks. Your reply has been stored.", resp.data) with self.journalist_app as journalist_app: resp = journalist_app.get(col_url) self.assertIn("reply-", resp.data) soup = BeautifulSoup(resp.data, '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 = self.journalist_app.post('/bulk', data=dict( filesystem_id=filesystem_id, action='download', doc_names_selected=checkbox_values ), follow_redirects=True) self.assertEqual(resp.status_code, 200) zf = zipfile.ZipFile(StringIO(resp.data), 'r') data = zf.read(zf.namelist()[0]) self._can_decrypt_with_key(data, config.JOURNALIST_KEY) self._can_decrypt_with_key(data, crypto_util.getkey(filesystem_id), codename) # Test deleting reply on the journalist interface last_reply_number = len( soup.select('input[name="doc_names_selected"]')) - 1 self.helper_filenames_delete(soup, last_reply_number) with self.source_app as source_app: resp = source_app.post('/login', data=dict(codename=codename), follow_redirects=True) self.assertEqual(resp.status_code, 200) resp = source_app.get('/lookup') self.assertEqual(resp.status_code, 200) if not expected_success: # there should be no reply self.assertNotIn("You have received a reply.", resp.data) else: self.assertIn( "You have received a reply. To protect your identity", resp.data) self.assertIn(test_reply, resp.data) soup = BeautifulSoup(resp.data, 'html.parser') msgid = soup.select( 'form.message > input[name="reply_filename"]')[0]['value'] resp = source_app.post('/delete', data=dict( filesystem_id=filesystem_id, reply_filename=msgid ), follow_redirects=True) self.assertEqual(resp.status_code, 200) self.assertIn("Reply deleted", resp.data) # Make sure the reply is deleted from the filesystem utils.async.wait_for_assertion( lambda: self.assertFalse(os.path.exists( store.path(filesystem_id, msgid)))) source_app.get('/logout')
def reply(): sid, msg = request.form['sid'], request.form['msg'] crypto_util.encrypt(crypto_util.getkey(sid), request.form['msg'], output= store.path(sid, 'reply-%s.gpg' % uuid.uuid4())) return render_template('reply.html', sid=sid, codename=db.display_id(sid, db.sqlalchemy_handle()))
def col(sid): source = get_source(sid) source.has_key = crypto_util.getkey(sid) return render_template("col.html", sid=sid, source=source)
def _block_on_reply_keypair_gen(codename): sid = crypto_util.hash_codename(codename) while not crypto_util.getkey(sid): sleep(0.1)
def helper_test_reply(self, test_reply, expected_success=True): test_msg = "This is a test message." with self.source_app as source_app: resp = source_app.get('/generate') resp = source_app.post('/create', follow_redirects=True) codename = session['codename'] sid = g.sid # redirected to submission form resp = source_app.post('/submit', data=dict( msg=test_msg, fh=(StringIO(''), ''), ), follow_redirects=True) self.assertEqual(resp.status_code, 200) self.assertFalse(g.source.flagged) source_app.get('/logout') resp = self.journalist_app.get('/') self.assertEqual(resp.status_code, 200) self.assertIn("Sources", resp.data) soup = BeautifulSoup(resp.data) col_url = soup.select('ul#cols > li a')[0]['href'] resp = self.journalist_app.get(col_url) self.assertEqual(resp.status_code, 200) with self.source_app as source_app: resp = source_app.post('/login', data=dict(codename=codename), follow_redirects=True) self.assertEqual(resp.status_code, 200) self.assertFalse(g.source.flagged) source_app.get('/logout') with self.journalist_app as journalist_app: resp = journalist_app.post('/flag', data=dict(sid=sid)) self.assertEqual(resp.status_code, 200) with self.source_app as source_app: resp = source_app.post('/login', data=dict(codename=codename), follow_redirects=True) self.assertEqual(resp.status_code, 200) self.assertTrue(g.source.flagged) source_app.get('/lookup') self.assertTrue(g.source.flagged) source_app.get('/logout') # Block up to 15s for the reply keypair, so we can test sending a reply utils. async .wait_for_assertion( lambda: self.assertNotEqual(crypto_util.getkey(sid), None), 15) # Create 2 replies to test deleting on journalist and source interface for i in range(2): resp = self.journalist_app.post('/reply', data=dict(sid=sid, msg=test_reply), follow_redirects=True) self.assertEqual(resp.status_code, 200) if not expected_success: pass else: self.assertIn("Thanks! Your reply has been stored.", resp.data) with self.journalist_app as journalist_app: resp = journalist_app.get(col_url) self.assertIn("reply-", resp.data) soup = BeautifulSoup(resp.data) # Download the reply and verify that it can be decrypted with the # journalist's key as well as the source's reply key sid = soup.select('input[name="sid"]')[0]['value'] checkbox_values = [ soup.select('input[name="doc_names_selected"]')[1]['value'] ] resp = self.journalist_app.post( '/bulk', data=dict(sid=sid, action='download', doc_names_selected=checkbox_values), follow_redirects=True) self.assertEqual(resp.status_code, 200) zf = zipfile.ZipFile(StringIO(resp.data), 'r') data = zf.read(zf.namelist()[0]) self._can_decrypt_with_key(data, config.JOURNALIST_KEY) self._can_decrypt_with_key(data, crypto_util.getkey(sid), codename) # Test deleting reply on the journalist interface last_reply_number = len( soup.select('input[name="doc_names_selected"]')) - 1 self.helper_filenames_delete(soup, last_reply_number) with self.source_app as source_app: resp = source_app.post('/login', data=dict(codename=codename), follow_redirects=True) self.assertEqual(resp.status_code, 200) resp = source_app.get('/lookup') self.assertEqual(resp.status_code, 200) if not expected_success: # there should be no reply self.assertNotIn("You have received a reply.", resp.data) else: self.assertIn( "You have received a reply. To protect your identity", resp.data) self.assertIn(test_reply, resp.data) soup = BeautifulSoup(resp.data) msgid = soup.select( 'form.message > input[name="reply_filename"]')[0]['value'] resp = source_app.post('/delete', data=dict(sid=sid, reply_filename=msgid), follow_redirects=True) self.assertEqual(resp.status_code, 200) self.assertIn("Reply deleted", resp.data) # Make sure the reply is deleted from the filesystem utils. async .wait_for_assertion(lambda: self.assertFalse( os.path.exists(store.path(sid, msgid)))) source_app.get('/logout')
def test_getkey(self): source, _ = utils.db_helper.init_source() self.assertIsNotNone(crypto_util.getkey(source.filesystem_id))