diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py index 0d79a5bd9120c6a19cc3f6eb83c8c354e8e07005..56dae0b1da1551bbce5d2bf47101747f28e41d74 100644 --- a/src/_zkapauthorizer/controller.py +++ b/src/_zkapauthorizer/controller.py @@ -419,6 +419,7 @@ class PaymentController(object): partial(self._redeemFailure, voucher), ) d.addErrback(partial(self._finalRedeemError, voucher)) + return d def _redeemSuccess(self, voucher, unblinded_tokens): """ diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py index 76a659da860617fd26dc39afaebe3b25be8cad3a..5fa84f84de2dc533925d94298d06b8a131b3ce87 100644 --- a/src/_zkapauthorizer/model.py +++ b/src/_zkapauthorizer/model.py @@ -340,6 +340,22 @@ class VoucherStore(object): in texts ) + @with_cursor + def backup(self, cursor): + """ + Read out all state necessary to recreate this database in the event it is + lost. + """ + cursor.execute( + """ + SELECT [token] FROM [unblinded-tokens] ORDER BY [token] + """, + ) + tokens = cursor.fetchall() + return { + u"unblinded-tokens": list(token for (token,) in tokens), + } + @attr.s(frozen=True) class UnblindedToken(object): diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py index 6e5409bbc16a696e076d1d9d8de64e0a1460223a..f06077759f89614dbc2ff1e0e86802653ff8343f 100644 --- a/src/_zkapauthorizer/resource.py +++ b/src/_zkapauthorizer/resource.py @@ -23,7 +23,9 @@ In the future it should also allow users to read statistics about token usage. from json import ( loads, dumps, ) - +from zope.interface import ( + Attribute, +) from twisted.logger import ( Logger, ) @@ -31,6 +33,7 @@ from twisted.web.http import ( BAD_REQUEST, ) from twisted.web.resource import ( + IResource, ErrorPage, NoResource, Resource, @@ -42,9 +45,17 @@ from ._base64 import ( from .controller import ( PaymentController, - NonRedeemer, + get_redeemer, ) +class IZKAPRoot(IResource): + """ + The root of the resource tree of this plugin's client web presence. + """ + store = Attribute("The ``VoucherStore`` used by this resource tree.") + controller = Attribute("The ``PaymentController`` used by this resource tree.") + + def from_configuration(node_config, store, redeemer=None): """ Instantiate the plugin root resource using data from its configuration @@ -63,13 +74,20 @@ def from_configuration(node_config, store, redeemer=None): :param IRedeemer redeemer: The voucher redeemer to use. If ``None`` a sensible one is constructed. - :return IResource: The root of the resource hierarchy presented by the + :return IZKAPRoot: The root of the resource hierarchy presented by the client side of the plugin. """ if redeemer is None: - redeemer = NonRedeemer() + redeemer = get_redeemer( + u"privatestorageio-zkapauthz-v1", + node_config, + None, + None, + ) controller = PaymentController(store, redeemer) root = Resource() + root.store = store + root.controller = controller root.putChild( b"voucher", _VoucherCollection( @@ -78,8 +96,8 @@ def from_configuration(node_config, store, redeemer=None): ), ) root.putChild( - b"blinded-token", - _BlindedTokenCollection( + b"unblinded-token", + _UnblindedTokenCollection( store, controller, ), @@ -96,10 +114,10 @@ def application_json(request): request.responseHeaders.setRawHeaders(u"content-type", [u"application/json"]) -class _BlindedTokenCollection(Resource): +class _UnblindedTokenCollection(Resource): """ - This class implements inspection of blinded tokens. Users **GET** this - resource to find out about blinded tokens in the system. + This class implements inspection of unblinded tokens. Users **GET** this + resource to find out about unblinded tokens in the system. """ _log = Logger() @@ -110,10 +128,15 @@ class _BlindedTokenCollection(Resource): def render_GET(self, request): """ - Retrieve some blinded tokens and associated information. + Retrieve some unblinded tokens and associated information. """ application_json(request) - return dumps({u"total": 0, u"blinded-tokens": []}) + state = self._store.backup() + unblinded_tokens = state[u"unblinded-tokens"] + return dumps({ + u"total": len(unblinded_tokens), + u"unblinded-tokens": unblinded_tokens, + }) diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py index 7011c7ca8e946faa61f2242bfec39eff451b5faa..46d2f5702cc1933313d3681963440df9e3f432b6 100644 --- a/src/_zkapauthorizer/tests/test_client_resource.py +++ b/src/_zkapauthorizer/tests/test_client_resource.py @@ -44,6 +44,10 @@ from testtools import ( from testtools.matchers import ( MatchesStructure, MatchesAll, + AllMatch, + HasLength, + IsInstance, + ContainsDict, AfterPreprocessing, Equals, Always, @@ -106,6 +110,8 @@ from ..resource import ( from .strategies import ( tahoe_configs, + client_dummyredeemer_configurations, + client_nonredeemer_configurations, vouchers, requests, ) @@ -234,8 +240,8 @@ class BlindedTokenTests(TestCase): self.useFixture(CaptureTwistedLogs()) - @given(tahoe_configs()) - def test_get(self, get_config): + @given(tahoe_configs(), vouchers(), integers(min_value=0, max_value=100)) + def test_get(self, get_config, voucher, num_tokens): """ When the blinded token collection receives a **GET**, the response is the total number of blinded tokens in the system and the blinded tokens @@ -244,10 +250,20 @@ class BlindedTokenTests(TestCase): tempdir = self.useFixture(TempDir()) config = get_config(tempdir.join(b"tahoe"), b"tub.port") root = root_from_config(config) + + if num_tokens: + # Put in a number of tokens with which to test. + redeeming = root.controller.redeem(voucher, num_tokens) + # Make sure the operation completed before proceeding. + self.assertThat( + redeeming, + succeeded(Always()), + ) + agent = RequestTraversalAgent(root) requesting = agent.request( b"GET", - b"http://127.0.0.1/blinded-token", + b"http://127.0.0.1/unblinded-token", ) self.addDetail( u"requesting result", @@ -261,11 +277,14 @@ class BlindedTokenTests(TestCase): AfterPreprocessing( json_content, succeeded( - Equals({ - u"total": 0, - u"blinded-tokens": [], + ContainsDict({ + u"total": Equals(num_tokens), + u"unblinded-tokens": MatchesAll( + HasLength(num_tokens), + AllMatch(IsInstance(unicode)), + ), }), - ) + ), ), ), ), @@ -395,14 +414,32 @@ class VoucherTests(TestCase): ), ) + @given(tahoe_configs(client_nonredeemer_configurations()), vouchers()) + def test_get_known_voucher_unredeemed(self, get_config, voucher): + """ + When a voucher is first ``PUT`` and then later a ``GET`` is issued for the + same voucher then the response code is **OK** and details about the + voucher are included in a json-encoded response body. + """ + return self._test_get_known_voucher(get_config, voucher, False) - @given(tahoe_configs(), vouchers()) - def test_get_known_voucher(self, get_config, voucher): + @given(tahoe_configs(client_dummyredeemer_configurations()), vouchers()) + def test_get_known_voucher_redeemed(self, get_config, voucher): """ When a voucher is first ``PUT`` and then later a ``GET`` is issued for the same voucher then the response code is **OK** and details about the voucher are included in a json-encoded response body. """ + return self._test_get_known_voucher(get_config, voucher, True) + + def _test_get_known_voucher(self, get_config, voucher, redeemed): + """ + Assert that a voucher that is ``PUT`` and then ``GET`` is represented in + the JSON response. + + :param bool redeemed: Whether the voucher is expected to be redeemed + or not in the response. + """ tempdir = self.useFixture(TempDir()) config = get_config(tempdir.join(b"tahoe"), b"tub.port") root = root_from_config(config) @@ -442,7 +479,7 @@ class VoucherTests(TestCase): AfterPreprocessing( json_content, succeeded( - Equals(Voucher(voucher).marshal()), + Equals(Voucher(voucher, redeemed=redeemed).marshal()), ), ), ), @@ -498,7 +535,7 @@ class VoucherTests(TestCase): succeeded( Equals({ u"vouchers": list( - Voucher(voucher).marshal() + Voucher(voucher, redeemed=True).marshal() for voucher in vouchers ),