From b8d050985f1d8a3c4c8b5b51fa1d5c8f61626e50 Mon Sep 17 00:00:00 2001
From: Jean-Paul Calderone <exarkun@twistedmatrix.com>
Date: Mon, 8 Nov 2021 14:09:58 -0500
Subject: [PATCH] refactor test code to support more kinds of failures more
 easily

---
 src/_zkapauthorizer/tests/test_controller.py | 48 ++++++++++++--------
 1 file changed, 28 insertions(+), 20 deletions(-)

diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py
index e7eceb4..a87548d 100644
--- a/src/_zkapauthorizer/tests/test_controller.py
+++ b/src/_zkapauthorizer/tests/test_controller.py
@@ -45,6 +45,7 @@ from testtools.matchers import (
     MatchesAll,
     MatchesStructure,
 )
+import attr
 from testtools.twistedsupport import failed, has_no_result, succeeded
 from treq.testing import StubTreq
 from twisted.internet.defer import fail
@@ -837,7 +838,7 @@ class RistrettoRedeemerTests(TestCase):
         ``AlreadySpent``.
         """
         num_tokens = counter + extra_tokens
-        issuer = AlreadySpentRedemption()
+        issuer = already_spent_redemption()
         treq = treq_for_loopback_ristretto(issuer)
         redeemer = RistrettoRedeemer(treq, NOWHERE)
         random_tokens = redeemer.random_tokens_for_voucher(voucher, counter, num_tokens)
@@ -865,7 +866,7 @@ class RistrettoRedeemerTests(TestCase):
         ``Unpaid``.
         """
         num_tokens = counter + extra_tokens
-        issuer = UnpaidRedemption()
+        issuer = unpaid_redemption()
         treq = treq_for_loopback_ristretto(issuer)
         redeemer = RistrettoRedeemer(treq, NOWHERE)
         random_tokens = redeemer.random_tokens_for_voucher(voucher, counter, num_tokens)
@@ -1040,34 +1041,41 @@ class UnexpectedResponseRedemption(Resource):
         return b"Sorry, this server does not behave well."
 
 
-class AlreadySpentRedemption(Resource):
+@attr.s
+class UnsuccessfulRedemption(Resource, object):
     """
-    An ``AlreadySpentRedemption`` simulates the Ristretto redemption server
-    but always refuses to allow vouchers to be redeemed and reports an error
-    that the voucher has already been redeemed.
+    A fake redemption server which always returns an unsuccessful response.
+
+    :ivar unicode reason: The value for the ``reason`` field of the result.
     """
+    reason = attr.ib()
+
+    def __attrs_post_init__(self):
+        Resource.__init__(self)
 
     def render_POST(self, request):
         request_error = check_redemption_request(request)
         if request_error is not None:
             return request_error
 
-        return bad_request(request, {u"success": False, u"reason": u"double-spend"})
+        return bad_request(request, {u"success": False, u"reason": self.reason})
 
-
-class UnpaidRedemption(Resource):
+def unpaid_redemption():
     """
-    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.
+    Return a fake Ristretto redemption server which always refuses to allow
+    vouchers to be redeemed and reports an error that the voucher has not been
+    paid for.
     """
+    return UnsuccessfulRedemption(u"unpaid")
 
-    def render_POST(self, request):
-        request_error = check_redemption_request(request)
-        if request_error is not None:
-            return request_error
 
-        return bad_request(request, {u"success": False, u"reason": u"unpaid"})
+def already_spent_redemption():
+    """
+    Return a fake Ristretto redemption server which always refuses to allow
+    vouchers to be redeemed and reports an error that the voucher has already
+    been redeemed.
+    """
+    return UnsuccessfulRedemption(u"double-spend")
 
 
 class RistrettoRedemption(Resource):
@@ -1124,7 +1132,7 @@ class CheckRedemptionRequestTests(TestCase):
         If the request content-type is not application/json, the response is
         **Unsupported Media Type**.
         """
-        issuer = UnpaidRedemption()
+        issuer = unpaid_redemption()
         treq = treq_for_loopback_ristretto(issuer)
         d = treq.post(
             NOWHERE.child(u"v1", u"redeem").to_text().encode("ascii"),
@@ -1145,7 +1153,7 @@ class CheckRedemptionRequestTests(TestCase):
         If the request body cannot be decoded as json, the response is **Bad
         Request**.
         """
-        issuer = UnpaidRedemption()
+        issuer = unpaid_redemption()
         treq = treq_for_loopback_ristretto(issuer)
         d = treq.post(
             NOWHERE.child(u"v1", u"redeem").to_text().encode("ascii"),
@@ -1178,7 +1186,7 @@ class CheckRedemptionRequestTests(TestCase):
         If the JSON object in the request body does not include all the necessary
         properties, the response is **Bad Request**.
         """
-        issuer = UnpaidRedemption()
+        issuer = unpaid_redemption()
         treq = treq_for_loopback_ristretto(issuer)
         d = treq.post(
             NOWHERE.child(u"v1", u"redeem").to_text().encode("ascii"),
-- 
GitLab