diff --git a/default.nix b/default.nix index 04193ed1586856a9b21d7403a0cde175071b3344..046aae00876256588381190b25c2a168134a8e4e 100644 --- a/default.nix +++ b/default.nix @@ -1,2 +1,2 @@ -{ pkgs ? import ./nixpkgs.nix { }, hypothesisProfile ? null, collectCoverage ? false, testSuite ? null, trialArgs ? [] }: +{ pkgs ? import ./nixpkgs.nix { }, hypothesisProfile ? null, collectCoverage ? false, testSuite ? null, trialArgs ? null }: pkgs.python27Packages.zkapauthorizer.override { inherit hypothesisProfile collectCoverage testSuite trialArgs; } diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py index 1939478bbba918a83170d061818cc8a882047855..21e0189b2e899564daebf3a07e9e6c3d27e5bbe6 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/resource.py b/src/_zkapauthorizer/resource.py index 1601d7054ca7f74bbb6553f18e2da20d4ebb3551..fb7e61aab584103c48ffee628e8835d28b73d9bb 100644 --- a/src/_zkapauthorizer/resource.py +++ b/src/_zkapauthorizer/resource.py @@ -196,7 +196,7 @@ class _VoucherCollection(Resource): application_json(request) return dumps({ u"vouchers": list( - voucher.marshal() + self._controller.incorporate_transient_state(voucher).marshal() for voucher in self._store.list() ), @@ -211,7 +211,6 @@ class _VoucherCollection(Resource): voucher = self._store.get(voucher) except KeyError: return NoResource() - # TODO Apply the same treatment to the list result return VoucherView(self._controller.incorporate_transient_state(voucher)) diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py index 22d2f544783374379d4ca340b2170d6d468f7d1b..653574423654bd9c5d0cb73da3c37f4b01406d4d 100644 --- a/src/_zkapauthorizer/tests/test_client_resource.py +++ b/src/_zkapauthorizer/tests/test_client_resource.py @@ -776,6 +776,59 @@ class VoucherTests(TestCase): A ``GET`` to the ``VoucherCollection`` itself returns a list of existing vouchers. """ + return self._test_list_vouchers( + get_config, + now, + vouchers, + Equals({ + u"vouchers": list( + Voucher( + voucher, + created=now, + state=Redeemed( + finished=now, + # Value duplicated from + # PaymentController.redeem + # default. Should do this better. + token_count=100, + ), + ).marshal() + for voucher + in vouchers + ), + }), + ) + + @given( + tahoe_configs(client_unpaidredeemer_configurations()), + datetimes(), + lists(vouchers(), unique=True), + ) + def test_list_vouchers_transient_states(self, get_config, now, vouchers): + """ + A ``GET`` to the ``VoucherCollection`` itself returns a list of existing + vouchers including state information that reflects transient states. + """ + return self._test_list_vouchers( + get_config, + now, + vouchers, + Equals({ + u"vouchers": list( + Voucher( + voucher, + created=now, + state=Unpaid( + finished=now, + ), + ).marshal() + for voucher + in vouchers + ), + }), + ) + + def _test_list_vouchers(self, get_config, now, vouchers, match_response_object): # Hypothesis causes our test case instances to be re-used many times # between setUp and tearDown. Avoid re-using the same temporary # directory for every Hypothesis iteration because this test leaves @@ -817,23 +870,7 @@ class VoucherTests(TestCase): AfterPreprocessing( json_content, succeeded( - Equals({ - u"vouchers": list( - Voucher( - voucher, - created=now, - state=Redeemed( - finished=now, - # Value duplicated from - # PaymentController.redeem - # default. Should do this better. - token_count=100, - ), - ).marshal() - for voucher - in vouchers - ), - }), + match_response_object, ), ), ), diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py index 31be5bf25240bcb663e260c297d3bfbc419522b4..86bcd8fa54d0ee95c13eefb6b748a8df3bd40f9b 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): diff --git a/tahoe-lafs.nix b/tahoe-lafs.nix index 5dd929dadf082dd8a657515aa1f4e4f2bb642228..c75f6dba03f346328446bf7256adf6900568d490 100644 --- a/tahoe-lafs.nix +++ b/tahoe-lafs.nix @@ -2,7 +2,7 @@ , twisted, foolscap, nevow, zfec , setuptools, setuptoolsTrial, pyasn1, zope_interface , service-identity, pyyaml, magic-wormhole, treq, appdirs -, eliot, autobahn, cryptography +, beautifulsoup4, eliot, autobahn, cryptography }: python.pkgs.buildPythonPackage rec { version = "1.14.0.dev"; @@ -10,10 +10,10 @@ python.pkgs.buildPythonPackage rec { src = fetchFromGitHub { owner = "LeastAuthority"; repo = "tahoe-lafs"; - # HEAD of an integration branch for all of the storage plugin stuff. Last - # updated October 4 2019. - rev = "8c1f536ba4fbc01f3bc5f08412edbefc56ff7037"; - sha256 = "17d7pkbsgss3rhqf7ac7ylzbddi555rnkzz48zjqwq1zx1z2jhy6"; + # A branch of master with the storage plugin web resource reuse issue + # resolved. https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3265 + rev = "1fef61981940bbd63ffc4242c3b589258622d117"; + sha256 = "0kgkg7wd0nkj8f5p46341vjkr6nz3kf0fimd44d9kypm4rn8xczv"; }; postPatch = '' @@ -33,8 +33,7 @@ python.pkgs.buildPythonPackage rec { twisted foolscap nevow zfec appdirs setuptoolsTrial pyasn1 zope_interface service-identity pyyaml magic-wormhole treq - - eliot autobahn cryptography setuptools + beautifulsoup4 eliot autobahn cryptography setuptools ]; checkInputs = with python.pkgs; [