From 660f34963288877445cffea1d25c4025669127ab Mon Sep 17 00:00:00 2001
From: Jean-Paul Calderone <exarkun@twistedmatrix.com>
Date: Tue, 12 Nov 2019 12:25:36 -0500
Subject: [PATCH] Get information for `unblinded-tokens` endpoint from the
 store

---
 src/_zkapauthorizer/controller.py             |  1 +
 src/_zkapauthorizer/model.py                  | 16 +++++
 src/_zkapauthorizer/resource.py               | 45 ++++++++++----
 .../tests/test_client_resource.py             | 59 +++++++++++++++----
 4 files changed, 99 insertions(+), 22 deletions(-)

diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py
index 0d79a5b..56dae0b 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 76a659d..5fa84f8 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 6e5409b..f060777 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 7011c7c..46d2f57 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
                                 ),
-- 
GitLab