From 188593ed4e5772524b2c151f560d9c1339b57b67 Mon Sep 17 00:00:00 2001
From: Jean-Paul Calderone <exarkun@twistedmatrix.com>
Date: Mon, 16 Dec 2019 10:54:31 -0500
Subject: [PATCH] A couple simple starter tests for lease maintenance

---
 src/_zkapauthorizer/lease_maintenance.py      | 27 +++++---
 src/_zkapauthorizer/tests/matchers.py         | 21 +++++++
 .../tests/test_lease_maintenance.py           | 61 +++++++++++++++++--
 3 files changed, 96 insertions(+), 13 deletions(-)

diff --git a/src/_zkapauthorizer/lease_maintenance.py b/src/_zkapauthorizer/lease_maintenance.py
index 3101909..ea4e920 100644
--- a/src/_zkapauthorizer/lease_maintenance.py
+++ b/src/_zkapauthorizer/lease_maintenance.py
@@ -268,6 +268,8 @@ def lease_maintenance_service(
         secret_holder,
         last_run,
         random,
+        interval_mean=None,
+        interval_range=None,
 ):
     """
     Get an ``IService`` which will maintain leases on ``root_node`` and any
@@ -293,15 +295,26 @@ def lease_maintenance_service(
 
     :param random: An object like ``random.Random`` which can be used as a
         source of scheduling delay.
+
+    :param timedelta interval_mean: The mean time between lease renewal checks.
+
+    :param timedelta interval_range: The range of the uniform distribution of
+        lease renewal checks (centered on ``interval_mean``).
     """
-    mean = timedelta(days=26).total_seconds()
-    halfrange = timedelta(days=2).total_seconds()
+    if interval_mean is None:
+        interval_mean = timedelta(days=26)
+    if interval_range is None:
+        interval_range = timedelta(days=4)
+    halfrange = interval_range / 2
+
     min_lease_remaining = timedelta(days=3)
-    sample_interval_distribution = partial(
-        random.uniform,
-        mean - halfrange,
-        mean + halfrange,
-    )
+    def sample_interval_distribution():
+        return timedelta(
+            seconds=random.uniform(
+                (interval_mean - halfrange).total_seconds(),
+                (interval_mean + halfrange).total_seconds(),
+            ),
+        )
     if last_run is None:
         initial_interval = sample_interval_distribution()
     else:
diff --git a/src/_zkapauthorizer/tests/matchers.py b/src/_zkapauthorizer/tests/matchers.py
index bcb4edb..0a52380 100644
--- a/src/_zkapauthorizer/tests/matchers.py
+++ b/src/_zkapauthorizer/tests/matchers.py
@@ -23,6 +23,11 @@ from testtools.matchers import (
     Mismatch,
     ContainsDict,
     Always,
+    MatchesAll,
+    MatchesAny,
+    GreaterThan,
+    LessThan,
+    Equals,
 )
 
 @attr.s
@@ -78,3 +83,19 @@ class _Returns(Matcher):
 
     def __str__(self):
         return "Returns({})".format(self.result_matcher)
+
+
+def between(low, high):
+    """
+    Matches a value in the range [low, high].
+    """
+    return MatchesAll(
+        MatchesAny(
+            Equals(low),
+            GreaterThan(low),
+        ),
+        MatchesAny(
+            Equals(high),
+            LessThan(high),
+        ),
+    )
diff --git a/src/_zkapauthorizer/tests/test_lease_maintenance.py b/src/_zkapauthorizer/tests/test_lease_maintenance.py
index b559dfd..5718c8b 100644
--- a/src/_zkapauthorizer/tests/test_lease_maintenance.py
+++ b/src/_zkapauthorizer/tests/test_lease_maintenance.py
@@ -22,7 +22,7 @@ from __future__ import (
 )
 
 from datetime import (
-    datetime,
+    # datetime,
     timedelta,
 )
 
@@ -31,12 +31,17 @@ import attr
 from testtools import (
     TestCase,
 )
+from hypothesis import (
+    given,
+)
 from hypothesis.strategies import (
     builds,
     binary,
     integers,
     lists,
+    floats,
     dictionaries,
+    randoms,
 )
 
 from twisted.internet.task import (
@@ -62,12 +67,13 @@ from ..foolscap import (
 
 from .matchers import (
     Provides,
+    between,
 )
 from .strategies import (
     storage_indexes,
 )
 
-from ..lease_maintenace import (
+from ..lease_maintenance import (
     lease_maintenance_service,
 )
 
@@ -140,13 +146,13 @@ class LeaseMaintenanceServiceTests(TestCase):
     """
     Tests for the service returned by ``lease_maintenance_service``.
     """
-    def test_interface(self):
+    @given(randoms())
+    def test_interface(self, random):
         """
         The service provides ``IService``.
         """
         clock = Clock()
         root_node = object()
-        random = object()
         lease_secret = b"\0" * CRYPTO_VAL_SIZE
         convergence_secret = b"\1" * CRYPTO_VAL_SIZE
         service = lease_maintenance_service(
@@ -154,10 +160,53 @@ class LeaseMaintenanceServiceTests(TestCase):
             root_node,
             DummyStorageBroker(clock, []),
             SecretHolder(lease_secret, convergence_secret),
-            datetime.utcfromtimestamp(0),
+            None,
             random,
         )
         self.assertThat(
             service,
-            Provides(IService),
+            Provides([IService]),
+        )
+
+    @given(
+        randoms(),
+        floats(
+            # It doesn't make sense to have a negative check interval mean.
+            min_value=0,
+            # We can't make this value too large or it isn't convertable to a
+            # timedelta.  Also, even values as large as this one are of
+            # questionable value.
+            max_value=60 * 60 * 24 * 365),
+    )
+    def test_initial_interval(self, random, mean):
+        """
+        When constructed without a value for ``last_run``,
+        ``lease_maintenance_service`` schedules its first run to take place
+        after an interval that falls uniformly in range centered on ``mean``
+        with a size of ``range``.
+        """
+        clock = Clock()
+        root_node = object()
+        lease_secret = b"\0" * CRYPTO_VAL_SIZE
+        convergence_secret = b"\1" * CRYPTO_VAL_SIZE
+
+        # Construct a range that fits in with the mean
+        range_ = random.uniform(0, mean)
+
+        service = lease_maintenance_service(
+            clock,
+            root_node,
+            DummyStorageBroker(clock, []),
+            SecretHolder(lease_secret, convergence_secret),
+            None,
+            random,
+            timedelta(seconds=mean),
+            timedelta(seconds=range_),
+        )
+        service.startService()
+
+        [maintenance_call] = clock.getDelayedCalls()
+        self.assertThat(
+            maintenance_call.getTime(),
+            between(mean - (range_ / 2), mean + (range_ / 2)),
         )
-- 
GitLab