[tor-commits] [bridgedb/master] Add several additional tests for bridgedb.email.distributor.
isis at torproject.org
isis at torproject.org
Sat Jul 25 19:26:22 UTC 2015
commit bb5b29dd19f32fb62d01a4d7ffb65b34493645e4
Author: Isis Lovecruft <isis at torproject.org>
Date: Wed Apr 22 02:58:31 2015 +0000
Add several additional tests for bridgedb.email.distributor.
---
lib/bridgedb/email/distributor.py | 12 +-
lib/bridgedb/test/test_email_distributor.py | 177 ++++++++++++++++++++++++++-
2 files changed, 182 insertions(+), 7 deletions(-)
diff --git a/lib/bridgedb/email/distributor.py b/lib/bridgedb/email/distributor.py
index b73c082..d8ea9bf 100644
--- a/lib/bridgedb/email/distributor.py
+++ b/lib/bridgedb/email/distributor.py
@@ -101,7 +101,7 @@ class EmailDistributor(Distributor):
def bridgesPerResponse(self, hashring=None):
return super(EmailDistributor, self).bridgesPerResponse(hashring)
- def getBridges(self, bridgeRequest, interval):
+ def getBridges(self, bridgeRequest, interval, clock=None):
"""Return a list of bridges to give to a user.
.. hint:: All checks on the email address (which should be stored in
@@ -119,6 +119,13 @@ class EmailDistributor(Distributor):
address.
:param interval: The time period when we got this request. This can be
any string, so long as it changes with every period.
+ :type clock: :api:`twisted.internet.task.Clock`
+ :param clock: If given, use the clock to ask what time it is, rather
+ than :api:`time.time`. This should likely only be used for
+ testing.
+ :rtype: list or ``None``
+ :returns: A list of :class:`~bridgedb.bridges.Bridges` for the
+ ``bridgeRequest.client``, if allowed. Otherwise, returns ``None``.
"""
if (not bridgeRequest.client) or (bridgeRequest.client == 'default'):
raise addr.BadEmail(
@@ -129,6 +136,9 @@ class EmailDistributor(Distributor):
now = time.time()
+ if clock:
+ now = clock.seconds()
+
with bridgedb.Storage.getDB() as db:
wasWarned = db.getWarnedEmail(bridgeRequest.client)
lastSaw = db.getEmailTime(bridgeRequest.client)
diff --git a/lib/bridgedb/test/test_email_distributor.py b/lib/bridgedb/test/test_email_distributor.py
index 96eb306..b4d88b2 100644
--- a/lib/bridgedb/test/test_email_distributor.py
+++ b/lib/bridgedb/test/test_email_distributor.py
@@ -15,6 +15,7 @@ import logging
import tempfile
import os
+from twisted.internet.task import Clock
from twisted.trial import unittest
import bridgedb.Storage
@@ -24,6 +25,7 @@ from bridgedb.email.distributor import EmailDistributor
from bridgedb.email.distributor import IgnoreEmail
from bridgedb.email.distributor import TooSoonEmail
from bridgedb.email.request import EmailBridgeRequest
+from bridgedb.parse.addr import BadEmail
from bridgedb.parse.addr import UnsupportedDomain
from bridgedb.parse.addr import normalizeEmail
from bridgedb.test.util import generateFakeBridges
@@ -37,13 +39,14 @@ BRIDGES = generateFakeBridges()
class EmailDistributorTests(unittest.TestCase):
"""Tests for :class:`bridgedb.email.distributor.EmailDistributor`."""
+ # Fail any tests which take longer than 15 seconds.
+ timeout = 15
+
def setUp(self):
- self.fd, self.fname = tempfile.mkstemp()
+ self.fd, self.fname = tempfile.mkstemp(suffix=".sqlite", dir=os.getcwd())
bridgedb.Storage.initializeDBLock()
self.db = bridgedb.Storage.openDatabase(self.fname)
bridgedb.Storage.setDBFilename(self.fname)
- self.cur = self.db.cursor()
- self.db.close()
self.bridges = BRIDGES
self.key = 'aQpeOFIj8q20s98awfoiq23rpOIjFaqpEWFoij1X'
@@ -68,7 +71,68 @@ class EmailDistributorTests(unittest.TestCase):
bridgeRequest.generateFilters()
return bridgeRequest
- def test_EmailDistributor_rate_limit(self):
+ def test_EmailDistributor_getBridges_default_client(self):
+ """If EmailBridgeRequest.client was not set, then getBridges() should
+ raise a bridgedb.parse.addr.BadEmail exception.
+ """
+ dist = EmailDistributor(self.key, self.domainmap, self.domainrules)
+ [dist.hashring.insert(bridge) for bridge in self.bridges]
+
+ # The "default" client is literally the string "default", see
+ # bridgedb.bridgerequest.BridgeRequestBase.
+ bridgeRequest = self.makeClientRequest('default')
+
+ self.assertRaises(BadEmail, dist.getBridges, bridgeRequest, 1)
+
+ def test_EmailDistributor_getBridges_with_whitelist(self):
+ """If an email address is in the whitelist, it should get a response
+ every time it asks (i.e. no rate-limiting).
+ """
+ # The whitelist should be in the form {EMAIL: GPG_FINGERPRINT}
+ whitelist = {'white at list.ed': '0123456789ABCDEF0123456789ABCDEF01234567'}
+ dist = EmailDistributor(self.key, self.domainmap, self.domainrules,
+ whitelist=whitelist)
+ [dist.hashring.insert(bridge) for bridge in self.bridges]
+
+ # A request from a whitelisted address should always get a response.
+ bridgeRequest = self.makeClientRequest('white at list.ed')
+ for i in range(5):
+ bridges = dist.getBridges(bridgeRequest, 1)
+ self.assertEqual(len(bridges), 3)
+
+ def test_EmailDistributor_getBridges_rate_limit_multiple_clients(self):
+ """Each client should be rate-limited separately."""
+ dist = EmailDistributor(self.key, self.domainmap, self.domainrules)
+ [dist.hashring.insert(bridge) for bridge in self.bridges]
+
+ bridgeRequest1 = self.makeClientRequest('abc at example.com')
+ bridgeRequest2 = self.makeClientRequest('def at example.com')
+ bridgeRequest3 = self.makeClientRequest('ghi at example.com')
+
+ # The first request from 'abc' should get a response with bridges.
+ self.assertEqual(len(dist.getBridges(bridgeRequest1, 1)), 3)
+ # The second from 'abc' gets a warning.
+ self.assertRaises(TooSoonEmail, dist.getBridges, bridgeRequest1, 1)
+ # The first request from 'def' should get a response with bridges.
+ self.assertEqual(len(dist.getBridges(bridgeRequest2, 1)), 3)
+ # The third from 'abc' is ignored.
+ self.assertRaises(IgnoreEmail, dist.getBridges, bridgeRequest1, 1)
+ # The second from 'def' gets a warning.
+ self.assertRaises(TooSoonEmail, dist.getBridges, bridgeRequest2, 1)
+ # The third from 'def' is ignored.
+ self.assertRaises(IgnoreEmail, dist.getBridges, bridgeRequest2, 1)
+ # The fourth from 'abc' is ignored.
+ self.assertRaises(IgnoreEmail, dist.getBridges, bridgeRequest1, 1)
+ # The first request from 'ghi' should get a response with bridges.
+ self.assertEqual(len(dist.getBridges(bridgeRequest3, 1)), 3)
+ # The second from 'ghi' gets a warning.
+ self.assertRaises(TooSoonEmail, dist.getBridges, bridgeRequest3, 1)
+ # The third from 'ghi' is ignored.
+ self.assertRaises(IgnoreEmail, dist.getBridges, bridgeRequest3, 1)
+ # The fourth from 'ghi' is ignored.
+ self.assertRaises(IgnoreEmail, dist.getBridges, bridgeRequest3, 1)
+
+ def test_EmailDistributor_getBridges_rate_limit(self):
"""A client's first email should return bridges. The second should
return a warning, and the third should receive no response.
"""
@@ -77,16 +141,117 @@ class EmailDistributorTests(unittest.TestCase):
bridgeRequest = self.makeClientRequest('abc at example.com')
- # The first request should get a response with bridges
+ # The first request should get a response with bridges.
bridges = dist.getBridges(bridgeRequest, 1)
self.assertGreater(len(bridges), 0)
[self.assertIsInstance(b, Bridge) for b in bridges]
self.assertEqual(len(bridges), 3)
- # The second gets a warning, and the third is ignored
+ # The second gets a warning, and the third is ignored.
self.assertRaises(TooSoonEmail, dist.getBridges, bridgeRequest, 1)
self.assertRaises(IgnoreEmail, dist.getBridges, bridgeRequest, 1)
+ def test_EmailDistributor_getBridges_rate_limit_expiry(self):
+ """A client's first email should return bridges. The second should
+ return a warning, and the third should receive no response. After the
+ EmailDistributor.emailRateMax is up, the client should be able to
+ receive a response again.
+ """
+ clock = Clock()
+ dist = EmailDistributor(self.key, self.domainmap, self.domainrules)
+ [dist.hashring.insert(bridge) for bridge in self.bridges]
+
+ bridgeRequest = self.makeClientRequest('abc at example.com')
+
+ # The first request should get a response with bridges.
+ self.assertEqual(len(dist.getBridges(bridgeRequest, 1, clock)), 3)
+ # The second gets a warning, and the rest are ignored.
+ self.assertRaises(TooSoonEmail, dist.getBridges, bridgeRequest, 1, clock)
+ self.assertRaises(IgnoreEmail, dist.getBridges, bridgeRequest, 1, clock)
+ self.assertRaises(IgnoreEmail, dist.getBridges, bridgeRequest, 1, clock)
+ self.assertRaises(IgnoreEmail, dist.getBridges, bridgeRequest, 1, clock)
+
+ clock.advance(2 * dist.emailRateMax)
+
+ # The client should again a response with bridges.
+ self.assertEqual(len(dist.getBridges(bridgeRequest, 1)), 3, clock)
+
+ def test_EmailDistributor_cleanDatabase(self):
+ """Calling cleanDatabase() should cleanup email times in database, but
+ not allow clients who have been recently warned and/or ignored to
+ receive a response again until the remainder of their MAX_EMAIL_RATE
+ time is up.
+ """
+ dist = EmailDistributor(self.key, self.domainmap, self.domainrules)
+ [dist.hashring.insert(bridge) for bridge in self.bridges]
+
+ bridgeRequest = self.makeClientRequest('abc at example.com')
+
+ # The first request should get a response with bridges.
+ self.assertEqual(len(dist.getBridges(bridgeRequest, 1)), 3)
+ # The second gets a warning, and the third is ignored.
+ self.assertRaises(TooSoonEmail, dist.getBridges, bridgeRequest, 1)
+ self.assertRaises(IgnoreEmail, dist.getBridges, bridgeRequest, 1)
+
+ dist.cleanDatabase()
+
+ # Cleaning the warning email times in the database shouldn't cause
+ # 'abc at example.com' to be able to email again, because only the times
+ # which aren't older than EMAIL_MAX_RATE should be cleared.
+ self.assertRaises(IgnoreEmail, dist.getBridges, bridgeRequest, 1)
+
+ def test_EmailDistributor_prepopulateRings(self):
+ """Calling prepopulateRings() should add two rings to the
+ EmailDistributor.hashring.
+ """
+ dist = EmailDistributor(self.key, self.domainmap, self.domainrules)
+
+ # There shouldn't be any subrings yet.
+ self.assertEqual(len(dist.hashring.filterRings), 0)
+
+ dist.prepopulateRings()
+
+ # There should now be two subrings, but the subrings should be empty.
+ self.assertEqual(len(dist.hashring.filterRings), 2)
+ for (filtre, subring) in dist.hashring.filterRings.values():
+ self.assertEqual(len(subring), 0)
+
+ # The subrings in this Distributor have gross names, because the
+ # filter functions (including their addresses in memory!) are used as
+ # the subring names. In this case, we should have something like:
+ #
+ # frozenset([<function byIPv6 at 0x7eff7ad7fc80>])
+ #
+ # and
+ #
+ # frozenset([<function byIPv4 at 0x7eff7ad7fc08>])
+ #
+ # So we have to join the strings together and check the whole thing,
+ # since we have no other way to use these stupid subring names to
+ # index into the dictionary they are stored in, because the memory
+ # addresses are unknowable until runtime.
+
+ # There should be an IPv4 subring and an IPv6 ring:
+ ringnames = dist.hashring.filterRings.keys()
+ self.failUnlessIn("IPv4", "".join([str(ringname) for ringname in ringnames]))
+ self.failUnlessIn("IPv6", "".join([str(ringname) for ringname in ringnames]))
+
+ [dist.hashring.insert(bridge) for bridge in self.bridges]
+
+ # There should still be two subrings.
+ self.assertEqual(len(dist.hashring.filterRings), 2)
+ for (filtre, subring) in dist.hashring.filterRings.values():
+ self.assertGreater(len(subring), 0)
+
+ # Ugh, the hashring code is so gross looking.
+ subrings = dist.hashring.filterRings
+ subring1 = subrings.values()[0][1]
+ subring2 = subrings.values()[1][1]
+ # Each subring should have roughly the same number of bridges.
+ # (Having ±10 bridges in either ring, out of 500 bridges total, should
+ # be so bad.)
+ self.assertApproximates(len(subring1), len(subring2), 10)
+
def test_EmailDistributor_unsupported_domain(self):
"""An unsupported domain should raise an UnsupportedDomain exception."""
self.assertRaises(UnsupportedDomain, normalizeEmail,
More information about the tor-commits
mailing list