From 24e237f17b456d7cdda7521889e5c284833abac0 Mon Sep 17 00:00:00 2001
From: Jean-Paul Calderone <exarkun@twistedmatrix.com>
Date: Tue, 12 Nov 2019 13:32:47 -0500
Subject: [PATCH] Add the position query argument

---
 src/_zkapauthorizer/resource.py               | 13 +++-
 .../tests/test_client_resource.py             | 75 +++++++++++++++++--
 2 files changed, 80 insertions(+), 8 deletions(-)

diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py
index ab7571e..faad423 100644
--- a/src/_zkapauthorizer/resource.py
+++ b/src/_zkapauthorizer/resource.py
@@ -20,6 +20,9 @@ vouchers for fresh tokens.
 In the future it should also allow users to read statistics about token usage.
 """
 
+from itertools import (
+    islice,
+)
 from json import (
     loads, dumps,
 )
@@ -137,9 +140,17 @@ class _UnblindedTokenCollection(Resource):
         limit = request.args.get(b"limit", [None])[0]
         if limit is not None:
             limit = int(limit)
+
+        position = request.args.get(b"position", [b""])[0].decode("utf-8")
+
         return dumps({
             u"total": len(unblinded_tokens),
-            u"unblinded-tokens": unblinded_tokens[:limit],
+            u"unblinded-tokens": list(islice((
+                token
+                for token
+                in unblinded_tokens
+                if token > position
+            ), limit)),
         })
 
 
diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py
index 8f31967..a73b7bb 100644
--- a/src/_zkapauthorizer/tests/test_client_resource.py
+++ b/src/_zkapauthorizer/tests/test_client_resource.py
@@ -51,6 +51,7 @@ from testtools.matchers import (
     AfterPreprocessing,
     Equals,
     Always,
+    GreaterThan,
 )
 from testtools.twistedsupport import (
     CaptureTwistedLogs,
@@ -308,8 +309,52 @@ class UnblindedTokenTests(TestCase):
             succeeded_with_unblinded_tokens(num_tokens, min(num_tokens, limit)),
         )
 
+    @given(tahoe_configs(), vouchers(), integers(min_value=0, max_value=100), text(max_size=64))
+    def test_get_position(self, get_config, voucher, num_tokens, position):
+        """
+        When the unblinded token collection receives a **GET** with a **position**
+        query argument, it returns all unblinded tokens which sort greater
+        than the position and no others.
+        """
+        tempdir = self.useFixture(TempDir())
+        config = get_config(tempdir.join(b"tahoe"), b"tub.port")
+        root = root_from_config(config)
 
-def succeeded_with_unblinded_tokens(all_token_count, returned_token_count):
+        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/unblinded-token?position={}".format(
+                quote(position.encode("utf-8"), safe=b""),
+            ),
+        )
+        self.addDetail(
+            u"requesting result",
+            text_content(u"{}".format(vars(requesting.result))),
+        )
+        self.assertThat(
+            requesting,
+            succeeded_with_unblinded_tokens_with_matcher(
+                num_tokens,
+                AllMatch(
+                    MatchesAll(
+                        GreaterThan(position),
+                        IsInstance(unicode),
+                    ),
+                ),
+            ),
+        )
+
+
+def succeeded_with_unblinded_tokens_with_matcher(all_token_count, match_unblinded_tokens):
     """
     :return: A matcher which matches a Deferred which fires with a response
         like the one returned by the **unblinded-tokens** endpoint.
@@ -317,8 +362,8 @@ def succeeded_with_unblinded_tokens(all_token_count, returned_token_count):
     :param int all_token_count: The expected value in the ``total`` field of
         the response.
 
-    :param int returned_token_count: The expected number of tokens in the
-       ``unblinded-tokens`` field of the response.
+    :param match_unblinded_tokens: A matcher for the ``unblinded-tokens``
+        field of the response.
     """
     return succeeded(
         MatchesAll(
@@ -328,16 +373,32 @@ def succeeded_with_unblinded_tokens(all_token_count, returned_token_count):
                 succeeded(
                     ContainsDict({
                         u"total": Equals(all_token_count),
-                        u"unblinded-tokens": MatchesAll(
-                            HasLength(returned_token_count),
-                            AllMatch(IsInstance(unicode)),
-                        ),
+                        u"unblinded-tokens": match_unblinded_tokens,
                     }),
                 ),
             ),
         ),
     )
 
+def succeeded_with_unblinded_tokens(all_token_count, returned_token_count):
+    """
+    :return: A matcher which matches a Deferred which fires with a response
+        like the one returned by the **unblinded-tokens** endpoint.
+
+    :param int all_token_count: The expected value in the ``total`` field of
+        the response.
+
+    :param int returned_token_count: The expected number of tokens in the
+       ``unblinded-tokens`` field of the response.
+    """
+    return succeeded_with_unblinded_tokens_with_matcher(
+        all_token_count,
+        MatchesAll(
+            HasLength(returned_token_count),
+            AllMatch(IsInstance(unicode)),
+        )
+    )
+
 
 class VoucherTests(TestCase):
     """
-- 
GitLab