From 11e0cff40cb070d196ab61fb86e2fce22facbf46 Mon Sep 17 00:00:00 2001
From: Jean-Paul Calderone <exarkun@twistedmatrix.com>
Date: Tue, 3 Dec 2019 10:00:15 -0500
Subject: [PATCH] Factor safe Clock creation into a strategy for reuse

---
 src/_zkapauthorizer/tests/strategies.py      | 39 ++++++++++++++++++++
 src/_zkapauthorizer/tests/test_controller.py | 30 ++++-----------
 2 files changed, 46 insertions(+), 23 deletions(-)

diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py
index 223a7ec..4402065 100644
--- a/src/_zkapauthorizer/tests/strategies.py
+++ b/src/_zkapauthorizer/tests/strategies.py
@@ -19,6 +19,9 @@ Hypothesis strategies for property testing.
 from base64 import (
     urlsafe_b64encode,
 )
+from datetime import (
+    datetime,
+)
 
 import attr
 
@@ -39,6 +42,9 @@ from hypothesis.strategies import (
     datetimes,
 )
 
+from twisted.internet.task import (
+    Clock,
+)
 from twisted.web.test.requesthelper import (
     DummyRequest,
 )
@@ -541,3 +547,36 @@ def announcements():
     return just({
         u"ristretto-issuer-root-url": u"https://issuer.example.invalid/",
     })
+
+
+_POSIX_EPOCH = datetime.utcfromtimestamp(0)
+
+def posix_safe_datetimes():
+    """
+    Build datetime instances in a range that can be represented as floats
+    without losing microsecond precision.
+    """
+    return datetimes(
+        # I don't know that time-based parts of the system break down
+        # before the POSIX epoch but I don't know that they work, either.
+        # Don't time travel with this code.
+        min_value=_POSIX_EPOCH,
+        # Once we get far enough into the future we lose the ability to
+        # represent a timestamp with microsecond precision in a floating point
+        # number, which we do with any POSIX timestamp-like API (eg
+        # twisted.internet.task.Clock).  So don't go far enough into the
+        # future.
+        max_value=datetime(2200, 1, 1),
+    )
+
+
+def clocks(now=posix_safe_datetimes()):
+    """
+    Build ``twisted.internet.task.Clock`` instances set to a time built by
+    ``now``.
+    """
+    def clock_at_time(when):
+        c = Clock()
+        c.advance((when - _POSIX_EPOCH).total_seconds())
+        return c
+    return now.map(clock_at_time)
diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py
index 672c95f..5485b16 100644
--- a/src/_zkapauthorizer/tests/test_controller.py
+++ b/src/_zkapauthorizer/tests/test_controller.py
@@ -64,9 +64,6 @@ from hypothesis.strategies import (
 from twisted.python.url import (
     URL,
 )
-from twisted.internet.task import (
-    Clock,
-)
 from twisted.internet.defer import (
     fail,
 )
@@ -119,6 +116,7 @@ from .strategies import (
     tahoe_configs,
     vouchers,
     voucher_objects,
+    clocks,
 )
 from .matchers import (
     Provides,
@@ -127,8 +125,6 @@ from .fixtures import (
     TemporaryVoucherStore,
 )
 
-POSIX_EPOCH = datetime.utcfromtimestamp(0)
-
 class PaymentControllerTests(TestCase):
     """
     Tests for ``PaymentController``.
@@ -232,32 +228,20 @@ class PaymentControllerTests(TestCase):
 
     @given(
         tahoe_configs(),
-        datetimes(
-            # I don't know that time-based parts of the system break down
-            # before the POSIX epoch but I don't know that they work, either.
-            # Don't time travel with this code.
-            min_value=POSIX_EPOCH,
-            # Once we get far enough into the future we lose the ability to
-            # represent a timestamp with microsecond precision in a floating
-            # point number, which we do with Clock.  So don't go far enough
-            # into the future.
-            max_value=datetime(2200, 1, 1),
-        ),
+        clocks(),
         vouchers(),
     )
-    def test_redeem_error_after_delay(self, get_config, now, voucher):
+    def test_redeem_error_after_delay(self, get_config, clock, voucher):
         """
         When ``PaymentController`` receives a non-terminal error trying to redeem
         a voucher, after some time passes it tries to redeem the voucher
         again.
         """
-        clock = Clock()
-        clock.advance((now - POSIX_EPOCH).total_seconds())
-
+        datetime_now = lambda: datetime.utcfromtimestamp(clock.seconds())
         store = self.useFixture(
             TemporaryVoucherStore(
                 get_config,
-                lambda: datetime.utcfromtimestamp(clock.seconds()),
+                datetime_now,
             ),
         ).store
         controller = PaymentController(
@@ -272,7 +256,7 @@ class PaymentControllerTests(TestCase):
             MatchesAll(
                 IsInstance(model_Unpaid),
                 MatchesStructure(
-                    finished=Equals(now),
+                    finished=Equals(datetime_now()),
                 ),
             )
         )
@@ -288,7 +272,7 @@ class PaymentControllerTests(TestCase):
                 IsInstance(model_Unpaid),
                 MatchesStructure(
                     # At the new time, demonstrating the retry was performed.
-                    finished=Equals(now + interval),
+                    finished=Equals(datetime_now()),
                 ),
             ),
         )
-- 
GitLab