diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py index 62d65c89b447578dc28068a6a569148c16844c3f..d86a8b89178d69e3b24214ce2caa7beddbb52420 100644 --- a/src/_zkapauthorizer/controller.py +++ b/src/_zkapauthorizer/controller.py @@ -78,6 +78,14 @@ class Unpaid(Exception): """ +class UnrecognizedFailureReason(Exception): + """ + An attempt was made to redeem a voucher and the response contained an unknown reason. + + The redemption attempt may be automatically retried at some point. + """ + + @attr.s class RedemptionResult(object): """ @@ -522,6 +530,8 @@ class RistrettoRedeemer(object): elif reason == u"unpaid": raise Unpaid(voucher) + raise UnrecognizedFailureReason() + self._log.info( "Redeemed: {public_key} {proof} {count}", public_key=result[u"public-key"], diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py index a87548dfedcc3fc31d8d638c68a790b62c974a8e..51aa113fa6da7f1ccce4db2f61999f89a49e3908 100644 --- a/src/_zkapauthorizer/tests/test_controller.py +++ b/src/_zkapauthorizer/tests/test_controller.py @@ -58,6 +58,7 @@ from twisted.web.resource import ErrorPage, Resource from zope.interface import implementer from ..controller import ( + UnrecognizedFailureReason, AlreadySpent, DoubleSpendRedeemer, ErrorRedeemer, @@ -885,6 +886,34 @@ class RistrettoRedeemerTests(TestCase): ), ) + @given(voucher_objects(), voucher_counters(), integers(min_value=0, max_value=100)) + def test_redemption_unknown_response(self, voucher, counter, extra_tokens): + """ + If the issuer returns a failure without a recognizable reason then + ``RistrettoRedeemer.redeemWithCounter`` returns a ``Deferred`` that + fails with ``UnrecognizedFailureReason``. + """ + num_tokens = counter + extra_tokens + issuer = UnsuccessfulRedemption(u"mysterious") + treq = treq_for_loopback_ristretto(issuer) + redeemer = RistrettoRedeemer(treq, NOWHERE) + random_tokens = redeemer.random_tokens_for_voucher(voucher, counter, num_tokens) + d = redeemer.redeemWithCounter( + voucher, + counter, + random_tokens, + ) + self.assertThat( + d, + failed( + AfterPreprocessing( + lambda f: f.value, + IsInstance(UnrecognizedFailureReason), + ), + ), + ) + + @given(voucher_objects(), voucher_counters(), integers(min_value=0, max_value=100)) def test_bad_ristretto_redemption(self, voucher, counter, extra_tokens): """