diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py index 3cb4c1d015e04697a2a078cbf3ab58df7893fb35..7dc5146ddd15a11a8837a8b9af7b928069bc58b7 100644 --- a/src/_zkapauthorizer/controller.py +++ b/src/_zkapauthorizer/controller.py @@ -33,6 +33,7 @@ from functools import ( ) from json import ( dumps, + loads, ) from datetime import ( timedelta, @@ -75,7 +76,7 @@ from twisted.web.client import ( Agent, ) from treq import ( - json_content, + content, ) from treq.client import ( HTTPClient, @@ -103,6 +104,15 @@ from .model import ( RETRY_INTERVAL = timedelta(milliseconds=1) +@attr.s +class UnexpectedResponse(Exception): + """ + The issuer responded in an unexpected and unhandled way. + """ + code = attr.ib() + body = attr.ib() + + class AlreadySpent(Exception): """ An attempt was made to redeem a voucher which has already been redeemed. @@ -524,11 +534,12 @@ class RistrettoRedeemer(object): }), headers={b"content-type": b"application/json"}, ) + response_body = yield content(response) + try: - result = yield json_content(response) + result = loads(response_body) except ValueError: - self._log.failure("Parsing redeem response failed", response=response) - raise + raise UnexpectedResponse(response.code, response_body) success = result.get(u"success", False) if not success: @@ -933,8 +944,8 @@ class PaymentController(object): self._unpaid[voucher] = self.store.now() else: self._log.error( - "Redeeming random tokens for a voucher ({voucher}) failed: {reason}", - reason=reason, + "Redeeming random tokens for a voucher ({voucher}) failed: {reason!r}", + reason=reason.value, voucher=voucher, ) self._error[voucher] = model_Error( diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py index 3e8d6fce78ef80963977ce3137121c325b34c415..bec922bb0863dea1caee2df78cd783e713894d11 100644 --- a/src/_zkapauthorizer/tests/test_controller.py +++ b/src/_zkapauthorizer/tests/test_controller.py @@ -86,6 +86,7 @@ from twisted.web.http_headers import ( from twisted.web.http import ( UNSUPPORTED_MEDIA_TYPE, BAD_REQUEST, + INTERNAL_SERVER_ERROR, ) from treq.testing import ( StubTreq, @@ -111,6 +112,7 @@ from ..controller import ( IndexedRedeemer, RecordingRedeemer, PaymentController, + UnexpectedResponse, AlreadySpent, Unpaid, token_count_for_group, @@ -621,6 +623,39 @@ class RistrettoRedeemerTests(TestCase): ), ) + @given(voucher_objects(), voucher_counters(), integers(min_value=0, max_value=100)) + def test_non_json_response(self, voucher, counter, num_tokens): + """ + If the issuer responds with something that isn't JSON then the response is + logged and the ``Deferred`` fires with a ``Failure`` wrapping + ``UnexpectedResponse``. + """ + issuer = UnexpectedResponseRedemption() + 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, + Equals( + UnexpectedResponse( + INTERNAL_SERVER_ERROR, + b"Sorry, this server does not behave well.", + ), + ), + ), + ), + ) + @given(voucher_objects(), voucher_counters(), integers(min_value=0, max_value=100)) def test_redemption_denied_alreadyspent(self, voucher, counter, extra_tokens): """ @@ -827,6 +862,16 @@ def stub_agent(): return _StubAgent() +class UnexpectedResponseRedemption(Resource): + """ + An ``UnexpectedResponseRedemption`` simulates the Ristretto redemption + server but always returns a non-JSON error response. + """ + def render_POST(self, request): + request.setResponseCode(INTERNAL_SERVER_ERROR) + return b"Sorry, this server does not behave well." + + class AlreadySpentRedemption(Resource): """ An ``AlreadySpentRedemption`` simulates the Ristretto redemption server