diff --git a/src/_zkapauthorizer/tests/test_lease_maintenance.py b/src/_zkapauthorizer/tests/test_lease_maintenance.py
index 4db256baa94c5747e705856abd1f4fb3cdfe4cbd..bbd12c0e6bfbf7a8af9234e04c949ab09280269f 100644
--- a/src/_zkapauthorizer/tests/test_lease_maintenance.py
+++ b/src/_zkapauthorizer/tests/test_lease_maintenance.py
@@ -36,6 +36,8 @@ from testtools.matchers import (
     Always,
     HasLength,
     MatchesAll,
+    AllMatch,
+    GreaterThan,
     AfterPreprocessing,
 )
 from testtools.twistedsupport import (
@@ -53,6 +55,8 @@ from hypothesis.strategies import (
     floats,
     dictionaries,
     randoms,
+    composite,
+    just,
 )
 
 from twisted.internet.task import (
@@ -68,9 +72,9 @@ from twisted.application.service import (
 from allmydata.util.hashutil import (
     CRYPTO_VAL_SIZE,
 )
-# from allmydata.client import (
-#     SecretHolder,
-# )
+from allmydata.client import (
+    SecretHolder,
+)
 
 from ..foolscap import (
     ShareStat,
@@ -83,6 +87,7 @@ from .matchers import (
 from .strategies import (
     storage_indexes,
     clocks,
+    leaf_nodes,
     node_hierarchies,
 )
 
@@ -90,6 +95,7 @@ from ..lease_maintenance import (
     lease_maintenance_service,
     # maintain_leases_from_root,
     visit_storage_indexes_from_root,
+    renew_leases,
 )
 
 
@@ -129,6 +135,7 @@ class DummyStorageServer(object):
             self.buckets[idx]
             for idx
             in storage_indexes
+            if idx in self.buckets
         ))
 
     def get_lease_seed(self):
@@ -142,10 +149,11 @@ class DummyStorageServer(object):
 
 def lease_seeds():
     return binary(
-        min_size=CRYPTO_VAL_SIZE,
-        max_size=CRYPTO_VAL_SIZE,
+        min_size=20,
+        max_size=20,
     )
 
+
 def share_stats():
     return builds(
         ShareStat,
@@ -153,6 +161,7 @@ def share_stats():
         lease_expiration=integers(min_value=0, max_value=2 ** 31),
     )
 
+
 def storage_servers(clocks):
     return builds(
         DummyStorageServer,
@@ -171,10 +180,12 @@ class DummyStorageBroker(object):
         return self._storage_servers
 
 
-def storage_brokers(clocks):
-    return builds(
-        DummyStorageBroker,
-        lists(storage_servers(clocks)),
+@composite
+def storage_brokers(draw, clocks):
+    clock = draw(clocks)
+    return DummyStorageBroker(
+        clock,
+        draw(lists(storage_servers(just(clock)))),
     )
 
 
@@ -355,3 +366,74 @@ class VisitStorageIndexesFromRootTests(TestCase):
                 ),
             ),
         )
+
+
+class RenewLeasesTests(TestCase):
+    """
+    Tests for ``renew_leases``.
+    """
+    @given(storage_brokers(clocks()), lists(leaf_nodes()))
+    def test_renewed(self, storage_broker, nodes):
+        """
+        ``renew_leases`` renews the leases of shares on all storage servers which
+        have no more than the specified amount of time remaining on their
+        current lease.
+        """
+        lease_secret = b"\0" * CRYPTO_VAL_SIZE
+        convergence_secret = b"\1" * CRYPTO_VAL_SIZE
+        secret_holder = SecretHolder(lease_secret, convergence_secret)
+        min_lease_remaining = timedelta(days=3)
+
+        def get_now():
+            return datetime.utcfromtimestamp(
+                storage_broker.clock.seconds(),
+            )
+
+        def visit_assets(visit):
+            for node in nodes:
+                visit(node.get_storage_index())
+            return succeed(None)
+
+        d = renew_leases(
+            visit_assets,
+            storage_broker,
+            secret_holder,
+            min_lease_remaining,
+            get_now,
+        )
+        self.assertThat(
+            d,
+            succeeded(Always()),
+        )
+
+        relevant_storage_indexes = set(
+            node.get_storage_index()
+            for node
+            in nodes
+        )
+
+        self.assertThat(
+            storage_broker.get_connected_servers(),
+            AllMatch(
+                AfterPreprocessing(
+                    # Get share stats for storage indexes we should have
+                    # visited and maintained.
+                    lambda storage_server: list(
+                        stat
+                        for (storage_index, stat)
+                        in storage_server.buckets.items()
+                        if storage_index in relevant_storage_indexes
+                    ),
+                    AllMatch(
+                        AfterPreprocessing(
+                            # Lease expiration for anything visited must be
+                            # further in the future than min_lease_remaining,
+                            # either because it had time left or because we
+                            # renewed it.
+                            lambda share_stat: datetime.utcfromtimestamp(share_stat.lease_expiration),
+                            GreaterThan(get_now() + min_lease_remaining),
+                        ),
+                    ),
+                ),
+            ),
+        )