diff --git a/src/_zkapauthorizer/spending.py b/src/_zkapauthorizer/spending.py index 2e44de96f4af2157d9db61f1b425c42e9d733f8d..a2836e18f90eec1e13ff8c32edf311e25b8dad82 100644 --- a/src/_zkapauthorizer/spending.py +++ b/src/_zkapauthorizer/spending.py @@ -145,6 +145,7 @@ class PassGroup(object): self._factory._reset(self.passes) +@implementer(IPassFactory) @attr.s class SpendingController(object): """ diff --git a/src/_zkapauthorizer/tests/matchers.py b/src/_zkapauthorizer/tests/matchers.py index 79b4febf24c6cad07c30b10f837aa3e9a74b92f4..5ea2613373b6b2b10bb91113031f45ad8fbfd42c 100644 --- a/src/_zkapauthorizer/tests/matchers.py +++ b/src/_zkapauthorizer/tests/matchers.py @@ -54,7 +54,7 @@ class Provides(object): """ Match objects that provide all of a list of Zope Interface interfaces. """ - interfaces = attr.ib() + interfaces = attr.ib(validator=attr.validators.instance_of(list)) def match(self, obj): missing = set() diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py index 28028fd87ad78348725343f9ac19bf710c6eb040..0c448cda3cce269ab18715c4de2fa560837b80d8 100644 --- a/src/_zkapauthorizer/tests/strategies.py +++ b/src/_zkapauthorizer/tests/strategies.py @@ -813,3 +813,12 @@ def node_hierarchies(): ).filter( storage_indexes_are_distinct, ) + + +def pass_counts(): + """ + Build integers usable as a number of passes to work on. There is always + at least one pass in a group and there are never "too many", whatever that + means. + """ + return integers(min_value=1, max_value=2 ** 8) diff --git a/src/_zkapauthorizer/tests/test_spending.py b/src/_zkapauthorizer/tests/test_spending.py new file mode 100644 index 0000000000000000000000000000000000000000..62473bb8f288117a16189598b22fd457b611ccfb --- /dev/null +++ b/src/_zkapauthorizer/tests/test_spending.py @@ -0,0 +1,112 @@ +# Copyright 2019 PrivateStorage.io, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Tests for ``_zkapauthorizer.spending``. +""" + +from testtools import ( + TestCase, +) +from testtools.matchers import ( + Always, + MatchesAll, + MatchesStructure, + HasLength, +) +from testtools.twistedsupport import ( + succeeded, +) + +from hypothesis import ( + given, +) + +from twisted.python.filepath import ( + FilePath, +) + +from .strategies import ( + vouchers, + pass_counts, + posix_safe_datetimes, +) +from .matchers import ( + Provides, +) +from ..model import ( + VoucherStore, + open_and_initialize, + memory_connect, +) +from ..controller import ( + DummyRedeemer, + PaymentController, +) +from ..spending import ( + IPassGroup, + SpendingController, +) + +class PassGroupTests(TestCase): + """ + Tests for ``IPassGroup`` and the factories that create them. + """ + @given(vouchers(), pass_counts(), posix_safe_datetimes()) + def test_get(self, voucher, num_passes, now): + """ + ``IPassFactory.get`` returns an ``IPassGroup`` provider containing the + requested number of passes. + """ + redeemer = DummyRedeemer() + here = FilePath(u".") + store = VoucherStore( + pass_value=2 ** 15, + database_path=here, + now=lambda: now, + connection=open_and_initialize(here, memory_connect), + ) + # Make sure there are enough tokens for us to extract! + self.assertThat( + PaymentController( + store, + redeemer, + # Have to pass it here or to redeem, doesn't matter which. + default_token_count=num_passes, + # No value in splitting it into smaller groups in this case. + # Doing so only complicates the test by imposing a different + # minimum token count requirement (can't have fewer tokens + # than groups). + num_redemption_groups=1, + ).redeem( + voucher, + ), + succeeded(Always()), + ) + + pass_factory = SpendingController( + extract_unblinded_tokens=store.extract_unblinded_tokens, + tokens_to_passes=redeemer.tokens_to_passes, + ) + + group = pass_factory.get(u"message", num_passes) + self.assertThat( + group, + MatchesAll( + Provides([IPassGroup]), + MatchesStructure( + passes=HasLength(num_passes), + ), + ), + ) diff --git a/src/_zkapauthorizer/tests/test_storage_client.py b/src/_zkapauthorizer/tests/test_storage_client.py index ee5d1bc001923beb5957c0a3205e2b7bb8b650bb..5fd6b6d4c343788e05e584ab7fe7078080b73122 100644 --- a/src/_zkapauthorizer/tests/test_storage_client.py +++ b/src/_zkapauthorizer/tests/test_storage_client.py @@ -41,9 +41,6 @@ from testtools.twistedsupport import ( from hypothesis import ( given, ) -from hypothesis.strategies import ( - integers, -) from twisted.internet.defer import ( succeed, @@ -55,6 +52,10 @@ from .matchers import ( odd, ) +from .strategies import ( + pass_counts, +) + from ..api import ( MorePassesRequired, ) @@ -71,10 +72,6 @@ from .storage_common import ( ) -def pass_counts(): - return integers(min_value=1, max_value=2 ** 8) - - class CallWithPassesTests(TestCase): """ Tests for ``call_with_passes``.