From 5111d2186266c39bbd0ff04360956c2b806623ad Mon Sep 17 00:00:00 2001
From: Jean-Paul Calderone <exarkun@twistedmatrix.com>
Date: Mon, 18 Nov 2019 15:49:17 -0500
Subject: [PATCH] recognize unpaid and also double-spend

---
 src/_zkapauthorizer/controller.py            |  8 +++-
 src/_zkapauthorizer/tests/test_controller.py | 41 +++++++++++++++++++-
 2 files changed, 46 insertions(+), 3 deletions(-)

diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py
index 1939478..21e0189 100644
--- a/src/_zkapauthorizer/controller.py
+++ b/src/_zkapauthorizer/controller.py
@@ -395,9 +395,13 @@ class RistrettoRedeemer(object):
             self._log.failure("Parsing redeem response failed", response=response)
             raise
 
-        if result.get(u"failed", False):
-            if result.get(u"reason", None) == u"double-spend":
+        success = result.get(u"success", False)
+        if not success:
+            reason = result.get(u"reason", None)
+            if reason == u"double-spend":
                 raise AlreadySpent(voucher)
+            elif reason == u"unpaid":
+                raise Unpaid(voucher)
 
         self._log.info("Redeemed: {public-key} {proof} {signatures}", **result)
 
diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py
index 31be5bf..86bcd8f 100644
--- a/src/_zkapauthorizer/tests/test_controller.py
+++ b/src/_zkapauthorizer/tests/test_controller.py
@@ -100,6 +100,7 @@ from ..controller import (
     RistrettoRedeemer,
     PaymentController,
     AlreadySpent,
+    Unpaid,
 )
 
 from ..model import (
@@ -282,6 +283,32 @@ class RistrettoRedeemerTests(TestCase):
             ),
         )
 
+    @given(voucher_objects(), integers(min_value=1, max_value=100))
+    def test_redemption_denied_unpaid(self, voucher, num_tokens):
+        """
+        If the issuer declines to allow the voucher to be redeemed and gives a
+        reason that the voucher has not been paid for, ``RistrettoRedeem``
+        returns a ``Deferred`` that fires with a ``Failure`` wrapping
+        ``Unpaid``.
+        """
+        issuer = UnpaidRedemption()
+        treq = treq_for_loopback_ristretto(issuer)
+        redeemer = RistrettoRedeemer(treq, NOWHERE)
+        random_tokens = redeemer.random_tokens_for_voucher(voucher, num_tokens)
+        d = redeemer.redeem(
+            voucher,
+            random_tokens,
+        )
+        self.assertThat(
+            d,
+            failed(
+                AfterPreprocessing(
+                    lambda f: f.value,
+                    IsInstance(Unpaid),
+                ),
+            ),
+        )
+
     @given(voucher_objects(), integers(min_value=1, max_value=100))
     def test_bad_ristretto_redemption(self, voucher, num_tokens):
         """
@@ -439,8 +466,20 @@ class AlreadySpentRedemption(Resource):
         if request.requestHeaders.getRawHeaders(b"content-type") != ["application/json"]:
             return bad_content_type(request)
 
-        return bad_request(request, {u"failed": True, u"reason": u"double-spend"})
+        return bad_request(request, {u"success": False, u"reason": u"double-spend"})
+
+
+class UnpaidRedemption(Resource):
+    """
+    An ``UnpaidRedemption`` simulates the Ristretto redemption server but
+    always refuses to allow vouchers to be redeemed and reports an error that
+    the voucher has not been paid for.
+    """
+    def render_POST(self, request):
+        if request.requestHeaders.getRawHeaders(b"content-type") != ["application/json"]:
+            return bad_content_type(request)
 
+        return bad_request(request, {u"success": False, u"reason": u"unpaid"})
 
 
 class RistrettoRedemption(Resource):
-- 
GitLab