From 29b2c3a51b21b1fd4cd4ef89db4b665aa71254f0 Mon Sep 17 00:00:00 2001
From: Jean-Paul Calderone <exarkun@twistedmatrix.com>
Date: Mon, 20 Apr 2020 15:23:33 -0400
Subject: [PATCH] Verify expected behavior of redemption endpoint verifier

---
 src/_zkapauthorizer/tests/test_controller.py | 95 +++++++++++++++++++-
 1 file changed, 94 insertions(+), 1 deletion(-)

diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py
index 608b8ae..608ef72 100644
--- a/src/_zkapauthorizer/tests/test_controller.py
+++ b/src/_zkapauthorizer/tests/test_controller.py
@@ -60,6 +60,8 @@ from hypothesis import (
 from hypothesis.strategies import (
     integers,
     datetimes,
+    lists,
+    sampled_from,
 )
 from twisted.python.url import (
     URL,
@@ -74,6 +76,9 @@ from twisted.web.resource import (
     ErrorPage,
     Resource,
 )
+from twisted.web.http_headers import (
+    Headers,
+)
 from twisted.web.http import (
     UNSUPPORTED_MEDIA_TYPE,
     BAD_REQUEST,
@@ -614,7 +619,91 @@ class RistrettoRedemption(Resource):
         })
 
 
+class CheckRedemptionRequestTests(TestCase):
+    """
+    Tests for ``check_redemption_request``.
+    """
+    def test_content_type(self):
+        """
+        If the request content-type is not application/json, the response is
+        **Unsupported Media Type**.
+        """
+        issuer = UnpaidRedemption()
+        treq = treq_for_loopback_ristretto(issuer)
+        d = treq.post(
+            NOWHERE.child(u"v1", u"redeem").to_text().encode("ascii"),
+            b"{}",
+        )
+        self.assertThat(
+            d,
+            succeeded(
+                AfterPreprocessing(
+                    lambda response: response.code,
+                    Equals(UNSUPPORTED_MEDIA_TYPE),
+                ),
+            ),
+        )
+
+    def test_not_json(self):
+        """
+        If the request body cannot be decoded as json, the response is **Bad
+        Request**.
+        """
+        issuer = UnpaidRedemption()
+        treq = treq_for_loopback_ristretto(issuer)
+        d = treq.post(
+            NOWHERE.child(u"v1", u"redeem").to_text().encode("ascii"),
+            b"foo",
+            headers=Headers({u"content-type": [u"application/json"]}),
+        )
+        self.assertThat(
+            d,
+            succeeded(
+                AfterPreprocessing(
+                    lambda response: response.code,
+                    Equals(BAD_REQUEST),
+                ),
+            ),
+        )
+
+    @given(
+        lists(
+            sampled_from(
+                [u"redeemVoucher", u"redeemCounter", u"redeemTokens"],
+            ),
+            # Something must be missing if the length is no longer than 2
+            # because there are 3 required properties.
+            max_size=2,
+            unique=True,
+        ),
+    )
+    def test_missing_properties(self, properties):
+        """
+        If the JSON object in the request body does not include all the necessary
+        properties, the response is **Bad Request**.
+        """
+        issuer = UnpaidRedemption()
+        treq = treq_for_loopback_ristretto(issuer)
+        d = treq.post(
+            NOWHERE.child(u"v1", u"redeem").to_text().encode("ascii"),
+            dumps(dict.fromkeys(properties)),
+            headers=Headers({u"content-type": [u"application/json"]}),
+        )
+        self.assertThat(
+            d,
+            succeeded(
+                AfterPreprocessing(
+                    lambda response: response.code,
+                    Equals(BAD_REQUEST),
+                ),
+            ),
+        )
+
 def check_redemption_request(request):
+    """
+    Verify that the given request conforms to the redemption server's public
+    interface.
+    """
     if request.requestHeaders.getRawHeaders(b"content-type") != ["application/json"]:
         return bad_content_type(request)
 
@@ -622,7 +711,11 @@ def check_redemption_request(request):
     content = request.content.read()
     request.content.seek(p)
 
-    request_body = loads(content)
+    try:
+        request_body = loads(content)
+    except ValueError:
+        return bad_request(request, None)
+
     expected_keys = {u"redeemVoucher", u"redeemCounter", u"redeemTokens"}
     actual_keys = set(request_body.keys())
     if expected_keys != actual_keys:
-- 
GitLab