diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py index c0d5536009d07976a85e74109cd3ec5347d5e302..ec72c8d740b6548facfd5a98f2a09e100ed03c5c 100644 --- a/src/_zkapauthorizer/controller.py +++ b/src/_zkapauthorizer/controller.py @@ -33,6 +33,8 @@ from twisted.internet.defer import ( succeed, ) +import privacypass + from .foolscap import ( TOKEN_LENGTH, ) @@ -138,6 +140,27 @@ class DummyRedeemer(object): ) +@implementer(IRedeemer) +@attr.s +class RistrettoRedeemer(object): + _agent = attr.ib() + + def random_tokens_for_voucher(self, voucher, count): + return list( + RandomToken(privacypass.RandomToken.create().encode_base64().decode("ascii")) + for n + in range(count) + ) + + def redeem(self, voucher, random_tokens): + # The wrong implementation, of course. + return succeed(list( + Pass(text=u"tok-" + token.token_value) + for token + in random_tokens + )) + + @attr.s class PaymentController(object): """ diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py index 2969a01187eb56dfa3cec435846f4d021c40f7a6..318d96426bbd2496fb36b11055a14a53b7fe0918 100644 --- a/src/_zkapauthorizer/tests/test_controller.py +++ b/src/_zkapauthorizer/tests/test_controller.py @@ -16,11 +16,25 @@ Tests for ``_zkapauthorizer.controller``. """ +from json import ( + loads, + dumps, +) +from zope.interface import ( + implementer, +) from testtools import ( TestCase, ) from testtools.matchers import ( Equals, + MatchesAll, + AllMatch, + IsInstance, + HasLength, +) +from testtools.twistedsupport import ( + succeeded, ) from fixtures import ( @@ -30,22 +44,42 @@ from fixtures import ( from hypothesis import ( given, ) - +from hypothesis.strategies import ( + integers, +) +from twisted.internet.defer import ( + fail, +) +from twisted.web.iweb import ( + IAgent, +) +from twisted.web.resource import ( + Resource, +) +from treq.testing import ( + RequestTraversalAgent, +) from ..controller import ( + IRedeemer, NonRedeemer, DummyRedeemer, + RistrettoRedeemer, PaymentController, ) from ..model import ( memory_connect, VoucherStore, + Pass, ) from .strategies import ( tahoe_configs, vouchers, ) +from .matchers import ( + Provides, +) class PaymentControllerTests(TestCase): """ @@ -98,3 +132,99 @@ class PaymentControllerTests(TestCase): persisted_voucher.redeemed, Equals(True), ) + + +class RistrettoRedeemerTests(TestCase): + """ + Tests for ``RistrettoRedeemer``. + """ + def test_interface(self): + """ + An ``RistrettoRedeemer`` instance provides ``IRedeemer``. + """ + redeemer = RistrettoRedeemer(stub_agent()) + self.assertThat( + redeemer, + Provides([IRedeemer]), + ) + + @given(vouchers(), integers(min_value=1, max_value=100)) + def test_redemption(self, voucher, num_tokens): + """ + ``RistrettoRedeemer.redeem`` returns a ``Deferred`` that fires with a list + of ``Pass`` instances. + """ + public_key = u"pub foo-bar" + signatures = list(u"sig-{}".format(n) for n in range(num_tokens)) + proof = u"proof bar-foo" + + issuer = SuccessfulRedemption(public_key, signatures, proof) + agent = agent_for_loopback_ristretto(issuer) + redeemer = RistrettoRedeemer(agent) + random_tokens = redeemer.random_tokens_for_voucher(voucher, num_tokens) + # The redeemer gives back the requested number of tokens. + self.expectThat( + len(random_tokens), + Equals(num_tokens), + ) + d = redeemer.redeem( + voucher, + random_tokens, + ) + # Perform some very basic checks on the results. We won't verify the + # crypto here since we don't have a real Ristretto server. Such + # checks would fail. Some integration tests will verify that part of + # things. + self.assertThat( + d, + succeeded( + MatchesAll( + AllMatch( + IsInstance(Pass), + ), + HasLength(num_tokens), + ), + ), + ) + + +def agent_for_loopback_ristretto(local_issuer): + """ + Create an ``IAgent`` which can dispatch to a local issuer. + """ + v1 = Resource() + v1.putChild(b"redeem", local_issuer) + root = Resource() + root.putChild(b"v1", v1) + return RequestTraversalAgent(root) + + +class SuccessfulRedemption(Resource): + def __init__(self, public_key, signatures, proof): + Resource.__init__(self) + self.public_key = public_key + self.signatures = signatures + self.proof = proof + self.redemptions = [] + + def render_POST(self, request): + request_body = loads(request.content.read()) + voucher = request_body[u"redeemVoucher"] + tokens = request_body[u"redeemTokens"] + self.redemptions.append((voucher, tokens)) + return dumps({ + u"success": True, + u"public-key": self.public_key, + u"signatures": self.signatures, + u"proof": self.proof, + }) + + +@implementer(IAgent) +class _StubAgent(object): + def request(self, method, uri, headers=None, bodyProducer=None): + return fail(Exception("It's only a model.")) + + +def stub_agent(): + return _StubAgent()