diff --git a/pyproject.toml b/pyproject.toml index 72280c92ba5d9213a24c71a6777a1d0a1b1f6d34..acfa20be05d68ee4c95e101446cb70b8dcf16ca6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,3 +27,15 @@ directory = "misc" name = "Misc" showcontent = false + +[tool.black] +target-version = ['py27'] +extend-exclude = ''' +# A regex preceded with ^/ will apply only to files and directories +# in the root of the project. +^/src/_zkapauthorizer/_version.py +''' + +[tool.isort] +profile = "black" +skip = ["src/_zkapauthorizer/_version.py"] diff --git a/src/_zkapauthorizer/__init__.py b/src/_zkapauthorizer/__init__.py index f19d34ad0cfc6894480f9a08edc630083545855c..8fe710e41a1aa81cfddf973117d3aef3651588a3 100644 --- a/src/_zkapauthorizer/__init__.py +++ b/src/_zkapauthorizer/__init__.py @@ -17,5 +17,6 @@ __all__ = [ ] from ._version import get_versions -__version__ = get_versions()['version'] + +__version__ = get_versions()["version"] del get_versions diff --git a/src/_zkapauthorizer/_base64.py b/src/_zkapauthorizer/_base64.py index ed838f8ee1eafe9ee3eb434426635fb910c83883..473cb41cdaee3d4174dffcf7cc1ade7e5b63ac10 100644 --- a/src/_zkapauthorizer/_base64.py +++ b/src/_zkapauthorizer/_base64.py @@ -16,28 +16,19 @@ This module implements base64 encoding-related functionality. """ -from __future__ import ( - absolute_import, -) +from __future__ import absolute_import -from re import ( - compile as _compile, -) +from base64 import b64decode as _b64decode +from binascii import Error +from re import compile as _compile -from binascii import ( - Error, -) +_b64decode_validator = _compile(b"^[A-Za-z0-9-_]*={0,2}$") -from base64 import ( - b64decode as _b64decode, -) - -_b64decode_validator = _compile(b'^[A-Za-z0-9-_]*={0,2}$') def urlsafe_b64decode(s): """ Like ``base64.b64decode`` but with validation. """ if not _b64decode_validator.match(s): - raise Error('Non-base64 digit found') + raise Error("Non-base64 digit found") return _b64decode(s, altchars=b"-_") diff --git a/src/_zkapauthorizer/_plugin.py b/src/_zkapauthorizer/_plugin.py index caa0906615d615fe04f433723eee81a83b8ee2f3..cf331c6986e88adbb9a9e5349b714fc18eae7f66 100644 --- a/src/_zkapauthorizer/_plugin.py +++ b/src/_zkapauthorizer/_plugin.py @@ -18,78 +18,35 @@ Tahoe-LAFS. """ import random -from weakref import ( - WeakValueDictionary, -) -from datetime import ( - datetime, - timedelta, -) -from functools import ( - partial, -) +from datetime import datetime, timedelta +from functools import partial +from weakref import WeakValueDictionary import attr - -from zope.interface import ( - implementer, -) - -from twisted.logger import ( - Logger, -) -from twisted.python.filepath import ( - FilePath, -) -from twisted.internet.defer import ( - succeed, -) - -from allmydata.interfaces import ( - IFoolscapStoragePlugin, - IAnnounceableStorageServer, -) -from allmydata.node import ( - MissingConfigEntry, -) -from allmydata.client import ( - _Client, -) -from challenge_bypass_ristretto import ( - SigningKey, -) - -from .api import ( - ZKAPAuthorizerStorageServer, - ZKAPAuthorizerStorageClient, -) - -from .model import ( - VoucherStore, -) - -from .resource import ( - from_configuration as resource_from_configuration, -) -from .storage_common import ( - BYTES_PER_PASS, - get_configured_pass_value, -) -from .controller import ( - get_redeemer, -) -from .spending import ( - SpendingController, -) - +from allmydata.client import _Client +from allmydata.interfaces import IAnnounceableStorageServer, IFoolscapStoragePlugin +from allmydata.node import MissingConfigEntry +from challenge_bypass_ristretto import SigningKey +from twisted.internet.defer import succeed +from twisted.logger import Logger +from twisted.python.filepath import FilePath +from zope.interface import implementer + +from .api import ZKAPAuthorizerStorageClient, ZKAPAuthorizerStorageServer +from .controller import get_redeemer from .lease_maintenance import ( SERVICE_NAME, lease_maintenance_service, maintain_leases_from_root, ) +from .model import VoucherStore +from .resource import from_configuration as resource_from_configuration +from .spending import SpendingController +from .storage_common import BYTES_PER_PASS, get_configured_pass_value _log = Logger() + @implementer(IAnnounceableStorageServer) @attr.s class AnnounceableStorageServer(object): @@ -112,6 +69,7 @@ class ZKAPAuthorizer(object): forces different methods to use instance state to share a database connection. """ + name = attr.ib(default=u"privatestorageio-zkapauthz-v1") _stores = attr.ib(default=attr.Factory(WeakValueDictionary)) @@ -120,7 +78,7 @@ class ZKAPAuthorizer(object): :return VoucherStore: The database for the given node. At most one connection is made to the database per ``ZKAPAuthorizer`` instance. """ - key = node_config.get_config_path() + key = node_config.get_config_path() try: s = self._stores[key] except KeyError: @@ -128,7 +86,6 @@ class ZKAPAuthorizer(object): self._stores[key] = s return s - def _get_redeemer(self, node_config, announcement, reactor): """ :return IRedeemer: The voucher redeemer indicated by the given @@ -137,7 +94,6 @@ class ZKAPAuthorizer(object): """ return get_redeemer(self.name, node_config, announcement, reactor) - def get_storage_server(self, configuration, get_anonymous_storage_server): kwargs = configuration.copy() root_url = kwargs.pop(u"ristretto-issuer-root-url") @@ -163,7 +119,6 @@ class ZKAPAuthorizer(object): ), ) - def get_storage_client(self, node_config, announcement, get_rref): """ Create an ``IStorageClient`` that submits ZKAPs with certain requests in @@ -172,19 +127,19 @@ class ZKAPAuthorizer(object): ``node_config``. """ from twisted.internet import reactor + redeemer = self._get_redeemer(node_config, announcement, reactor) store = self._get_store(node_config) controller = SpendingController.for_store( tokens_to_passes=redeemer.tokens_to_passes, store=store, - ) + ) return ZKAPAuthorizerStorageClient( get_configured_pass_value(node_config), get_rref, controller.get, ) - def get_client_resource(self, node_config, reactor=None): """ Get an ``IZKAPRoot`` for the given node configuration. @@ -203,15 +158,20 @@ class ZKAPAuthorizer(object): _init_storage = _Client.__dict__["init_storage"] + + def maintenance_init_storage(self, announceable_storage_servers): """ A monkey-patched version of ``_Client.init_storage`` which also initializes the lease maintenance service. """ from twisted.internet import reactor + result = _init_storage(self, announceable_storage_servers) _maybe_attach_maintenance_service(reactor, self) return result + + _Client.init_storage = maintenance_init_storage @@ -252,12 +212,12 @@ def _create_maintenance_service(reactor, node_config, client_node): :param allmydata.client._Client client_node: The client node the lease maintenance service will be attached to. """ + def get_now(): return datetime.utcfromtimestamp(reactor.seconds()) - from twisted.plugins.zkapauthorizer import ( - storage_server, - ) + from twisted.plugins.zkapauthorizer import storage_server + store = storage_server._get_store(node_config) # Create the operation which performs the lease maintenance job when @@ -283,7 +243,9 @@ def _create_maintenance_service(reactor, node_config, client_node): progress=store.start_lease_maintenance, get_now=get_now, ) - last_run_path = FilePath(node_config.get_private_path(b"last-lease-maintenance-run")) + last_run_path = FilePath( + node_config.get_private_path(b"last-lease-maintenance-run") + ) # Create the service to periodically run the lease maintenance operation. return lease_maintenance_service( maintain_leases, diff --git a/src/_zkapauthorizer/_stack.py b/src/_zkapauthorizer/_stack.py index 6fffe2cd73a6d241322269101f32afef7dfc0d73..c6d553371b9e064583f2cc28bcc976236b5c9a51 100644 --- a/src/_zkapauthorizer/_stack.py +++ b/src/_zkapauthorizer/_stack.py @@ -12,26 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. -from contextlib import ( - contextmanager, -) +from contextlib import contextmanager try: - from resource import ( - RLIMIT_STACK, - getrlimit, - setrlimit, - ) + from resource import RLIMIT_STACK, getrlimit, setrlimit except ImportError: # Not available on Windows, unfortunately. RLIMIT_STACK = object() + def getrlimit(which): return (-1, -1) + def setrlimit(which, what): pass - @contextmanager def less_limited_stack(): """ diff --git a/src/_zkapauthorizer/_storage_client.py b/src/_zkapauthorizer/_storage_client.py index 656679768f898ae67a4a63ea2bac01f88bf9ec80..a52e18b49c2017662585ece9b81798cbf72d8c29 100644 --- a/src/_zkapauthorizer/_storage_client.py +++ b/src/_zkapauthorizer/_storage_client.py @@ -20,55 +20,29 @@ This is the client part of a storage access protocol. The server part is implemented in ``_storage_server.py``. """ -from __future__ import ( - absolute_import, -) +from __future__ import absolute_import -from functools import ( - partial, - wraps, -) +from functools import partial, wraps import attr -from attr.validators import ( - provides, -) - -from zope.interface import ( - implementer, -) - -from eliot.twisted import ( - inline_callbacks, -) - -from twisted.internet.interfaces import ( - IReactorTime, -) -from twisted.python.reflect import ( - namedAny, -) -from twisted.internet.defer import ( - returnValue, -) -from allmydata.interfaces import ( - IStorageServer, -) - -from .eliot import ( - SIGNATURE_CHECK_FAILED, - CALL_WITH_PASSES, -) - +from allmydata.interfaces import IStorageServer +from attr.validators import provides +from eliot.twisted import inline_callbacks +from twisted.internet.defer import returnValue +from twisted.internet.interfaces import IReactorTime +from twisted.python.reflect import namedAny +from zope.interface import implementer + +from .eliot import CALL_WITH_PASSES, SIGNATURE_CHECK_FAILED from .storage_common import ( MorePassesRequired, + add_lease_message, + allocate_buckets_message, + get_required_new_passes_for_mutable_write, + has_writes, pass_value_attribute, required_passes, - allocate_buckets_message, - add_lease_message, slot_testv_and_readv_and_writev_message, - has_writes, - get_required_new_passes_for_mutable_write, ) @@ -78,6 +52,7 @@ class IncorrectStorageServerReference(Exception): server instead references some other kind of object. This makes the connection, and thus the configured storage server, unusable. """ + def __init__(self, furl, actual_name, expected_name): self.furl = furl self.actual_name = actual_name @@ -119,7 +94,9 @@ def invalidate_rejected_passes(passes, more_passes_required): # suite... but let's not be so vulgar. return None SIGNATURE_CHECK_FAILED.log(count=num_failed) - rejected_passes, okay_passes = passes.split(more_passes_required.signature_check_failed) + rejected_passes, okay_passes = passes.split( + more_passes_required.signature_check_failed + ) rejected_passes.mark_invalid(u"signature check failed") # It would be great to just expand okay_passes right here. However, if @@ -184,7 +161,9 @@ def call_with_passes_with_manual_spend(method, num_passes, get_passes, on_succes pass_group = okay_pass_group # Add the necessary number of new passes. This might # fail if we don't have enough tokens. - pass_group = pass_group.expand(num_passes - len(pass_group.passes)) + pass_group = pass_group.expand( + num_passes - len(pass_group.passes) + ) else: on_success(result, pass_group) break @@ -221,9 +200,11 @@ def with_rref(f): The ``RemoteReference`` is retrieved by calling ``_rref`` on the first argument passed to the function (expected to be ``self``). """ + @wraps(f) def g(self, *args, **kwargs): return f(self, self._rref(), *args, **kwargs) + return g @@ -233,11 +214,7 @@ def _encode_passes(group): :return list[bytes]: The encoded form of the passes in the given group. """ - return list( - t.pass_text.encode("ascii") - for t - in group.passes - ) + return list(t.pass_text.encode("ascii") for t in group.passes) @implementer(IStorageServer) @@ -265,6 +242,7 @@ class ZKAPAuthorizerStorageClient(object): request for which they will be used. The second gives the number of passes to request. """ + _expected_remote_interface_name = ( "RIPrivacyPassAuthorizedStorageServer.tahoe.privatestorage.io" ) @@ -302,10 +280,10 @@ class ZKAPAuthorizerStorageClient(object): ) def _spend_for_allocate_buckets( - self, - allocated_size, - result, - pass_group, + self, + allocated_size, + result, + pass_group, ): """ Spend some subset of a pass group based on the results of an @@ -336,16 +314,18 @@ class ZKAPAuthorizerStorageClient(object): @with_rref def allocate_buckets( - self, - rref, - storage_index, - renew_secret, - cancel_secret, - sharenums, - allocated_size, - canary, + self, + rref, + storage_index, + renew_secret, + cancel_secret, + sharenums, + allocated_size, + canary, ): - num_passes = required_passes(self._pass_value, [allocated_size] * len(sharenums)) + num_passes = required_passes( + self._pass_value, [allocated_size] * len(sharenums) + ) return call_with_passes_with_manual_spend( lambda passes: rref.callRemote( "allocate_buckets", @@ -358,15 +338,18 @@ class ZKAPAuthorizerStorageClient(object): canary, ), num_passes, - partial(self._get_passes, allocate_buckets_message(storage_index).encode("utf-8")), + partial( + self._get_passes, + allocate_buckets_message(storage_index).encode("utf-8"), + ), partial(self._spend_for_allocate_buckets, allocated_size), ) @with_rref def get_buckets( - self, - rref, - storage_index, + self, + rref, + storage_index, ): return rref.callRemote( "get_buckets", @@ -376,17 +359,19 @@ class ZKAPAuthorizerStorageClient(object): @inline_callbacks @with_rref def add_lease( - self, - rref, - storage_index, - renew_secret, - cancel_secret, + self, + rref, + storage_index, + renew_secret, + cancel_secret, ): - share_sizes = (yield rref.callRemote( - "share_sizes", - storage_index, - None, - )).values() + share_sizes = ( + yield rref.callRemote( + "share_sizes", + storage_index, + None, + ) + ).values() num_passes = required_passes(self._pass_value, share_sizes) result = yield call_with_passes( @@ -411,12 +396,12 @@ class ZKAPAuthorizerStorageClient(object): @with_rref def advise_corrupt_share( - self, - rref, - share_type, - storage_index, - shnum, - reason, + self, + rref, + share_type, + storage_index, + shnum, + reason, ): return rref.callRemote( "advise_corrupt_share", @@ -429,12 +414,12 @@ class ZKAPAuthorizerStorageClient(object): @inline_callbacks @with_rref def slot_testv_and_readv_and_writev( - self, - rref, - storage_index, - secrets, - tw_vectors, - r_vector, + self, + rref, + storage_index, + secrets, + tw_vectors, + r_vector, ): # Read operations are free. num_passes = 0 @@ -459,8 +444,7 @@ class ZKAPAuthorizerStorageClient(object): now = self._clock.seconds() current_sizes = { sharenum: stat.size - for (sharenum, stat) - in stats.items() + for (sharenum, stat) in stats.items() if stat.lease_expiration > now } # Determine the cost of the new storage for the operation. @@ -489,11 +473,11 @@ class ZKAPAuthorizerStorageClient(object): @with_rref def slot_readv( - self, - rref, - storage_index, - shares, - r_vector, + self, + rref, + storage_index, + shares, + r_vector, ): return rref.callRemote( "slot_readv", diff --git a/src/_zkapauthorizer/_storage_server.py b/src/_zkapauthorizer/_storage_server.py index a7a5616f35ee934868c5a3ae3b33c1953bbbb98a..85149180aad1d4ecfe32237e280780d30a9e8c7a 100644 --- a/src/_zkapauthorizer/_storage_server.py +++ b/src/_zkapauthorizer/_storage_server.py @@ -21,97 +21,46 @@ This is the server part of a storage access protocol. The client part is implemented in ``_storage_client.py``. """ -from __future__ import ( - absolute_import, -) - -from struct import ( - unpack, - calcsize, -) - -from errno import ( - ENOENT, -) +from __future__ import absolute_import -from functools import ( - partial, -) +from datetime import timedelta +from errno import ENOENT +from functools import partial +from os import listdir, stat +from os.path import join +from struct import calcsize, unpack -from os.path import ( - join, -) -from os import ( - listdir, - stat, -) -from datetime import ( - timedelta, -) import attr -from attr.validators import ( - provides, - instance_of, -) - -from zope.interface import ( - implementer_only, -) -from foolscap.api import ( - Referenceable, -) -from foolscap.ipb import ( - IReferenceable, - IRemotelyCallable, -) -from allmydata.interfaces import ( - RIStorageServer, -) -from allmydata.storage.common import ( - storage_index_to_dir, -) -from allmydata.util.base32 import ( - b2a, -) -from challenge_bypass_ristretto import ( - TokenPreimage, - VerificationSignature, - SigningKey, -) - -from twisted.internet.defer import ( - Deferred, -) -from twisted.python.reflect import ( - namedAny, -) -from twisted.internet.interfaces import ( - IReactorTime, -) - -from eliot import ( - start_action, -) - -from .foolscap import ( - ShareStat, - RIPrivacyPassAuthorizedStorageServer, -) +from allmydata.interfaces import RIStorageServer +from allmydata.storage.common import storage_index_to_dir +from allmydata.util.base32 import b2a +from attr.validators import instance_of, provides +from challenge_bypass_ristretto import SigningKey, TokenPreimage, VerificationSignature +from eliot import start_action +from foolscap.api import Referenceable +from foolscap.ipb import IReferenceable, IRemotelyCallable +from twisted.internet.defer import Deferred +from twisted.internet.interfaces import IReactorTime +from twisted.python.reflect import namedAny +from zope.interface import implementer_only + +from .foolscap import RIPrivacyPassAuthorizedStorageServer, ShareStat from .storage_common import ( MorePassesRequired, + add_lease_message, + allocate_buckets_message, + get_required_new_passes_for_mutable_write, + has_writes, pass_value_attribute, required_passes, - allocate_buckets_message, - add_lease_message, slot_testv_and_readv_and_writev_message, - has_writes, - get_required_new_passes_for_mutable_write, ) # See allmydata/storage/mutable.py SLOT_HEADER_SIZE = 468 LEASE_TRAILER_SIZE = 4 + @attr.s class _ValidationResult(object): """ @@ -123,6 +72,7 @@ class _ValidationResult(object): :ivar list[int] signature_check_failed: A list of indexes (into the validated list) of passes which did not have a correct signature. """ + valid = attr.ib() signature_check_failed = attr.ib() @@ -145,7 +95,9 @@ class _ValidationResult(object): proposed_signature = VerificationSignature.decode_base64(signature_base64) unblinded_token = signing_key.rederive_unblinded_token(preimage) verification_key = unblinded_token.derive_verification_key_sha512() - invalid_pass = verification_key.invalid_sha512(proposed_signature, message.encode("utf-8")) + invalid_pass = verification_key.invalid_sha512( + proposed_signature, message.encode("utf-8") + ) return invalid_pass except Exception: # It would be pretty nice to log something here, sometimes, I guess? @@ -195,7 +147,9 @@ class LeaseRenewalRequired(Exception): """ -@implementer_only(RIPrivacyPassAuthorizedStorageServer, IReferenceable, IRemotelyCallable) +@implementer_only( + RIPrivacyPassAuthorizedStorageServer, IReferenceable, IRemotelyCallable +) # It would be great to use `frozen=True` (value-based hashing) instead of # `cmp=False` (identity based hashing) but Referenceable wants to set some # attributes on self and it's hard to avoid that. @@ -229,7 +183,16 @@ class ZKAPAuthorizerStorageServer(Referenceable): """ return self._original.remote_get_version() - def remote_allocate_buckets(self, passes, storage_index, renew_secret, cancel_secret, sharenums, allocated_size, canary): + def remote_allocate_buckets( + self, + passes, + storage_index, + renew_secret, + cancel_secret, + sharenums, + allocated_size, + canary, + ): """ Pass-through after a pass check to ensure that clients can only allocate storage for immutable shares if they present valid passes. @@ -310,8 +273,8 @@ class ZKAPAuthorizerStorageServer(Referenceable): def remote_share_sizes(self, storage_index_or_slot, sharenums): with start_action( - action_type=u"zkapauthorizer:storage-server:remote:share-sizes", - storage_index_or_slot=storage_index_or_slot, + action_type=u"zkapauthorizer:storage-server:remote:share-sizes", + storage_index_or_slot=storage_index_or_slot, ): return dict( get_share_sizes(self._original, storage_index_or_slot, sharenums) @@ -320,17 +283,16 @@ class ZKAPAuthorizerStorageServer(Referenceable): def remote_stat_shares(self, storage_indexes_or_slots): return list( dict(stat_share(self._original, storage_index_or_slot)) - for storage_index_or_slot - in storage_indexes_or_slots + for storage_index_or_slot in storage_indexes_or_slots ) def remote_slot_testv_and_readv_and_writev( - self, - passes, - storage_index, - secrets, - tw_vectors, - r_vector, + self, + passes, + storage_index, + secrets, + tw_vectors, + r_vector, ): """ Pass-through after a pass check to ensure clients can only allocate @@ -341,9 +303,9 @@ class ZKAPAuthorizerStorageServer(Referenceable): same from the perspective of pass validation. """ with start_action( - action_type=u"zkapauthorizer:storage-server:remote:slot-testv-and-readv-and-writev", - storage_index=b2a(storage_index), - path=storage_index_to_dir(storage_index), + action_type=u"zkapauthorizer:storage-server:remote:slot-testv-and-readv-and-writev", + storage_index=b2a(storage_index), + path=storage_index_to_dir(storage_index), ): result = self._slot_testv_and_readv_and_writev( passes, @@ -357,12 +319,12 @@ class ZKAPAuthorizerStorageServer(Referenceable): return result def _slot_testv_and_readv_and_writev( - self, - passes, - storage_index, - secrets, - tw_vectors, - r_vector, + self, + passes, + storage_index, + secrets, + tw_vectors, + r_vector, ): # Only writes to shares without an active lease will result in a lease # renewal. @@ -380,11 +342,13 @@ class ZKAPAuthorizerStorageServer(Referenceable): ) if has_active_lease(self._original, storage_index, self._clock.seconds()): # Some of the storage is paid for already. - current_sizes = dict(get_share_sizes( - self._original, - storage_index, - tw_vectors.keys(), - )) + current_sizes = dict( + get_share_sizes( + self._original, + storage_index, + tw_vectors.keys(), + ) + ) # print("has writes, has active lease, current sizes: {}".format(current_sizes)) else: # None of it is. @@ -434,11 +398,7 @@ def has_active_lease(storage_server, storage_index, now): with an expiration time after ``now``. """ leases = storage_server.get_slot_leases(storage_index) - return any( - lease.get_expiration_time() > now - for lease - in leases - ) + return any(lease.get_expiration_time() > now for lease in leases) def check_pass_quantity(pass_value, validation, share_sizes): @@ -463,7 +423,9 @@ def check_pass_quantity(pass_value, validation, share_sizes): validation.raise_for(required_pass_count) -def check_pass_quantity_for_lease(pass_value, storage_index, validation, storage_server): +def check_pass_quantity_for_lease( + pass_value, storage_index, validation, storage_server +): """ Check that the given number of passes is sufficient to add or renew a lease for one period for the given storage index. @@ -567,8 +529,9 @@ def get_share_sizes(storage_server, storage_index_or_slot, sharenums): """ return ( (sharenum, stat.size) - for (sharenum, stat) - in get_share_stats(storage_server, storage_index_or_slot, sharenums) + for (sharenum, stat) in get_share_stats( + storage_server, storage_index_or_slot, sharenums + ) ) @@ -592,7 +555,9 @@ def get_share_stats(storage_server, storage_index_or_slot, sharenums): is a share number and the second element gives stats about that share. """ stat = None - for sharenum, sharepath in get_all_share_paths(storage_server, storage_index_or_slot): + for sharenum, sharepath in get_all_share_paths( + storage_server, storage_index_or_slot + ): if stat is None: stat = get_stat(sharepath) if sharenums is None or sharenum in sharenums: @@ -699,7 +664,7 @@ def get_slot_share_size(sharepath): """ with open(sharepath, "rb") as share_file: share_data_length_bytes = share_file.read(92)[-8:] - (share_data_length,) = unpack('>Q', share_data_length_bytes) + (share_data_length,) = unpack(">Q", share_data_length_bytes) return share_data_length @@ -711,7 +676,9 @@ def stat_share(storage_server, storage_index_or_slot): ``ShareStat``. """ stat = None - for sharenum, sharepath in get_all_share_paths(storage_server, storage_index_or_slot): + for sharenum, sharepath in get_all_share_paths( + storage_server, storage_index_or_slot + ): if stat is None: stat = get_stat(sharepath) yield (sharenum, stat(storage_server, storage_index_or_slot, sharepath)) @@ -733,16 +700,12 @@ def get_stat(sharepath): return stat_bucket +from foolscap.ipb import ISlicer +from foolscap.referenceable import ReferenceableSlicer + # I don't understand why this is required. # ZKAPAuthorizerStorageServer is-a Referenceable. It seems like # the built in adapter should take care of this case. -from twisted.python.components import ( - registerAdapter, -) -from foolscap.referenceable import ( - ReferenceableSlicer, -) -from foolscap.ipb import ( - ISlicer, -) +from twisted.python.components import registerAdapter + registerAdapter(ReferenceableSlicer, ZKAPAuthorizerStorageServer, ISlicer) diff --git a/src/_zkapauthorizer/api.py b/src/_zkapauthorizer/api.py index 70e4725e5c89d503e6973e2b708cecede48bcd66..f4bb2c21bfbbf1fcce7ca906538b1a802c677043 100644 --- a/src/_zkapauthorizer/api.py +++ b/src/_zkapauthorizer/api.py @@ -20,17 +20,10 @@ __all__ = [ "ZKAPAuthorizer", ] -from .storage_common import ( - MorePassesRequired, -) -from ._storage_server import ( - LeaseRenewalRequired, - ZKAPAuthorizerStorageServer, -) -from ._storage_client import ( - ZKAPAuthorizerStorageClient, -) +from ._storage_client import ZKAPAuthorizerStorageClient +from ._storage_server import LeaseRenewalRequired, ZKAPAuthorizerStorageServer +from .storage_common import MorePassesRequired -from ._plugin import ( - ZKAPAuthorizer, -) +# This needs to be imported after the above, since it imports those things from here. +# isort: split +from ._plugin import ZKAPAuthorizer diff --git a/src/_zkapauthorizer/configutil.py b/src/_zkapauthorizer/configutil.py index d87772d9052f63157a0bb83235a1bb5004eb5e9d..2df26fc6d8257e950f7ea29d2a51d61ad31ea16f 100644 --- a/src/_zkapauthorizer/configutil.py +++ b/src/_zkapauthorizer/configutil.py @@ -16,12 +16,7 @@ Basic utilities related to the Tahoe configuration file. """ -from __future__ import ( - division, - absolute_import, - print_function, - unicode_literals, -) +from __future__ import absolute_import, division, print_function, unicode_literals def _merge_dictionaries(dictionaries): @@ -62,14 +57,15 @@ def config_string_from_sections(divided_sections): last value wins). """ sections = _merge_dictionaries(divided_sections) - return "".join(list( - "[{name}]\n{items}\n".format( - name=name, - items="\n".join( - "{key} = {value}".format(key=key, value=_tahoe_config_quote(value)) - for (key, value) - in contents.items() + return "".join( + list( + "[{name}]\n{items}\n".format( + name=name, + items="\n".join( + "{key} = {value}".format(key=key, value=_tahoe_config_quote(value)) + for (key, value) in contents.items() + ), ) + for (name, contents) in sections.items() ) - for (name, contents) in sections.items() - )) + ) diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py index 44a4b0211864796234874aca316066330635132b..62d65c89b447578dc28068a6a569148c16844c3f 100644 --- a/src/_zkapauthorizer/controller.py +++ b/src/_zkapauthorizer/controller.py @@ -17,98 +17,48 @@ This module implements controllers (in the MVC sense) for the web interface for the client side of the storage plugin. """ -from __future__ import ( - absolute_import, -) - -from sys import ( - exc_info, -) -from operator import ( - setitem, - delitem, -) -from functools import ( - partial, -) -from json import ( - dumps, - loads, -) -from datetime import ( - timedelta, -) -from base64 import ( - b64encode, - b64decode, -) -from hashlib import ( - sha256, -) +from __future__ import absolute_import -import attr - -from zope.interface import ( - Interface, - implementer, -) - -from twisted.python.reflect import ( - namedAny, -) -from twisted.logger import ( - Logger, -) -from twisted.python.url import ( - URL, -) -from twisted.internet.defer import ( - Deferred, - succeed, - fail, - inlineCallbacks, - returnValue, -) -from twisted.internet.task import ( - LoopingCall, -) -from twisted.web.client import ( - Agent, -) -from treq import ( - content, -) -from treq.client import ( - HTTPClient, -) +from base64 import b64decode, b64encode +from datetime import timedelta +from functools import partial +from hashlib import sha256 +from json import dumps, loads +from operator import delitem, setitem +from sys import exc_info +import attr import challenge_bypass_ristretto - -from ._base64 import ( - urlsafe_b64decode, -) -from ._stack import ( - less_limited_stack, -) - -from .model import ( - RandomToken, - UnblindedToken, - Voucher, - Pass, - Pending as model_Pending, - Unpaid as model_Unpaid, - Redeeming as model_Redeeming, - Error as model_Error, -) +from treq import content +from treq.client import HTTPClient +from twisted.internet.defer import Deferred, fail, inlineCallbacks, returnValue, succeed +from twisted.internet.task import LoopingCall +from twisted.logger import Logger +from twisted.python.reflect import namedAny +from twisted.python.url import URL +from twisted.web.client import Agent +from zope.interface import Interface, implementer + +from ._base64 import urlsafe_b64decode +from ._stack import less_limited_stack +from .model import Error as model_Error +from .model import Pass +from .model import Pending as model_Pending +from .model import RandomToken +from .model import Redeeming as model_Redeeming +from .model import UnblindedToken +from .model import Unpaid as model_Unpaid +from .model import Voucher RETRY_INTERVAL = timedelta(milliseconds=1000) + @attr.s class UnexpectedResponse(Exception): """ The issuer responded in an unexpected and unhandled way. """ + code = attr.ib() body = attr.ib() @@ -139,6 +89,7 @@ class RedemptionResult(object): :ivar unicode public_key: The public key which the server proved was involved in the redemption process. """ + unblinded_tokens = attr.ib() public_key = attr.ib() @@ -147,6 +98,7 @@ class IRedeemer(Interface): """ An ``IRedeemer`` can exchange a voucher for one or more passes. """ + def random_tokens_for_voucher(voucher, counter, count): """ Generate a number of random tokens to use in the redemption process for @@ -222,6 +174,7 @@ class IndexedRedeemer(object): A ``IndexedRedeemer`` delegates redemption to a redeemer chosen to correspond to the redemption counter given. """ + _log = Logger() redeemers = attr.ib() @@ -248,6 +201,7 @@ class NonRedeemer(object): """ A ``NonRedeemer`` never tries to redeem vouchers for ZKAPs. """ + @classmethod def make(cls, section_name, node_config, announcement, reactor): return cls() @@ -272,6 +226,7 @@ class ErrorRedeemer(object): An ``ErrorRedeemer`` immediately locally fails voucher redemption with a configured error. """ + details = attr.ib(validator=attr.validators.instance_of(unicode)) @classmethod @@ -301,6 +256,7 @@ class DoubleSpendRedeemer(object): A ``DoubleSpendRedeemer`` pretends to try to redeem vouchers for ZKAPs but always fails with an error indicating the voucher has already been spent. """ + @classmethod def make(cls, section_name, node_config, announcement, reactor): return cls() @@ -319,6 +275,7 @@ class UnpaidRedeemer(object): An ``UnpaidRedeemer`` pretends to try to redeem vouchers for ZKAPs but always fails with an error indicating the voucher has not been paid for. """ + @classmethod def make(cls, section_name, node_config, announcement, reactor): return cls() @@ -337,6 +294,7 @@ class RecordingRedeemer(object): A ``CountingRedeemer`` delegates redemption logic to another object but records all redemption attempts. """ + original = attr.ib() redemptions = attr.ib(default=attr.Factory(list)) @@ -350,6 +308,7 @@ class RecordingRedeemer(object): def dummy_random_tokens(voucher, counter, count): v = urlsafe_b64decode(voucher.number.encode("ascii")) + def dummy_random_token(n): return RandomToken( # Padding is 96 (random token length) - 32 (decoded voucher @@ -358,11 +317,8 @@ def dummy_random_tokens(voucher, counter, count): v + u"{:0>4}{:0>60}".format(counter, n).encode("ascii"), ).decode("ascii"), ) - return list( - dummy_random_token(n) - for n - in range(count) - ) + + return list(dummy_random_token(n) for n in range(count)) @implementer(IRedeemer) @@ -379,6 +335,7 @@ class DummyRedeemer(object): Its corresponding private key certainly has not been used to sign anything. """ + _public_key = attr.ib( validator=attr.validators.instance_of(unicode), ) @@ -410,17 +367,15 @@ class DummyRedeemer(object): voucher, ), ) + def dummy_unblinded_token(random_token): random_value = b64decode(random_token.token_value.encode("ascii")) unblinded_value = random_value + b"x" * (96 - len(random_value)) return UnblindedToken(b64encode(unblinded_value).decode("ascii")) + return succeed( RedemptionResult( - list( - dummy_unblinded_token(token) - for token - in random_tokens - ), + list(dummy_unblinded_token(token) for token in random_tokens), self._public_key, ), ) @@ -431,21 +386,20 @@ class DummyRedeemer(object): # can include in the resulting Pass. This ensures the pass values # will be unique if and only if the unblinded tokens were unique # (barring improbable hash collisions). - token_digest = sha256( - token.unblinded_token.encode("ascii") - ).hexdigest().encode("ascii") + token_digest = ( + sha256(token.unblinded_token.encode("ascii")) + .hexdigest() + .encode("ascii") + ) - preimage = b"preimage-" + token_digest[len(b"preimage-"):] - signature = b"signature-" + token_digest[len(b"signature-"):] + preimage = b"preimage-" + token_digest[len(b"preimage-") :] + signature = b"signature-" + token_digest[len(b"signature-") :] return Pass( b64encode(preimage).decode("ascii"), b64encode(signature).decode("ascii"), ) - return list( - token_to_pass(token) - for token - in unblinded_tokens - ) + + return list(token_to_pass(token) for token in unblinded_tokens) class IssuerConfigurationMismatch(Exception): @@ -470,6 +424,7 @@ class IssuerConfigurationMismatch(Exception): ZKAPs and chooses to change its configured issuer address, those existing ZKAPs will not be usable and new ones must be obtained. """ + def __str__(self): return "Announced issuer ({}) disagrees with configured issuer ({}).".format( *self.args @@ -489,6 +444,7 @@ class RistrettoRedeemer(object): :ivar URL _api_root: The root of the issuer HTTP API. """ + _log = Logger() _treq = attr.ib() @@ -522,31 +478,33 @@ class RistrettoRedeemer(object): def random_tokens_for_voucher(self, voucher, counter, count): return list( RandomToken( - challenge_bypass_ristretto.RandomToken.create().encode_base64().decode("ascii"), + challenge_bypass_ristretto.RandomToken.create() + .encode_base64() + .decode("ascii"), ) - for n - in range(count) + for n in range(count) ) @inlineCallbacks def redeemWithCounter(self, voucher, counter, encoded_random_tokens): random_tokens = list( - challenge_bypass_ristretto.RandomToken.decode_base64(token.token_value.encode("ascii")) - for token - in encoded_random_tokens + challenge_bypass_ristretto.RandomToken.decode_base64( + token.token_value.encode("ascii") + ) + for token in encoded_random_tokens ) blinded_tokens = list(token.blind() for token in random_tokens) response = yield self._treq.post( self._api_root.child(u"v1", u"redeem").to_text(), - dumps({ - u"redeemVoucher": voucher.number, - u"redeemCounter": counter, - u"redeemTokens": list( - token.encode_base64() - for token - in blinded_tokens - ), - }), + dumps( + { + u"redeemVoucher": voucher.number, + u"redeemCounter": counter, + u"redeemTokens": list( + token.encode_base64() for token in blinded_tokens + ), + } + ), headers={b"content-type": b"application/json"}, ) response_body = yield content(response) @@ -583,8 +541,7 @@ class RistrettoRedeemer(object): challenge_bypass_ristretto.SignedToken.decode_base64( marshaled_signed_token.encode("ascii"), ) - for marshaled_signed_token - in marshaled_signed_tokens + for marshaled_signed_token in marshaled_signed_tokens ) self._log.info("Decoded signed tokens") clients_proof = challenge_bypass_ristretto.BatchDLEQProof.decode_base64( @@ -601,45 +558,39 @@ class RistrettoRedeemer(object): self._log.info("Validated proof") unblinded_tokens = list( UnblindedToken(token.encode_base64().decode("ascii")) - for token - in clients_unblinded_tokens + for token in clients_unblinded_tokens + ) + returnValue( + RedemptionResult( + unblinded_tokens, + marshaled_public_key, + ) ) - returnValue(RedemptionResult( - unblinded_tokens, - marshaled_public_key, - )) def tokens_to_passes(self, message, unblinded_tokens): assert isinstance(message, bytes) assert isinstance(unblinded_tokens, list) assert all(isinstance(element, UnblindedToken) for element in unblinded_tokens) unblinded_tokens = list( - challenge_bypass_ristretto.UnblindedToken.decode_base64(token.unblinded_token.encode("ascii")) - for token - in unblinded_tokens + challenge_bypass_ristretto.UnblindedToken.decode_base64( + token.unblinded_token.encode("ascii") + ) + for token in unblinded_tokens ) clients_verification_keys = list( - token.derive_verification_key_sha512() - for token - in unblinded_tokens + token.derive_verification_key_sha512() for token in unblinded_tokens ) clients_signatures = list( verification_key.sign_sha512(message) - for verification_key - in clients_verification_keys - ) - clients_preimages = list( - token.preimage() - for token - in unblinded_tokens + for verification_key in clients_verification_keys ) + clients_preimages = list(token.preimage() for token in unblinded_tokens) passes = list( Pass( preimage.encode_base64().decode("ascii"), signature.encode_base64().decode("ascii"), ) - for (preimage, signature) - in zip(clients_preimages, clients_signatures) + for (preimage, signature) in zip(clients_preimages, clients_signatures) ) return passes @@ -732,6 +683,7 @@ class PaymentController(object): :ivar IReactorTime _clock: The reactor to use for scheduling redemption retries. """ + _log = Logger() store = attr.ib() @@ -801,7 +753,9 @@ class PaymentController(object): voucher=voucher.number, ) - def _get_random_tokens_for_voucher(self, voucher, counter, num_tokens, total_tokens): + def _get_random_tokens_for_voucher( + self, voucher, counter, num_tokens, total_tokens + ): """ Generate or load random tokens for a redemption attempt of a voucher. @@ -810,6 +764,7 @@ class PaymentController(object): :param int total_tokens: The total number of tokens for which this voucher is expected to be redeemed. """ + def get_tokens(): self._log.info( "Generating random tokens for a voucher ({voucher}).", @@ -878,7 +833,9 @@ class PaymentController(object): # server signs a given set of random tokens once or many times, the # number of passes that can be constructed is still only the size of # the set of random tokens. - token_count = token_count_for_group(self.num_redemption_groups, num_tokens, counter) + token_count = token_count_for_group( + self.num_redemption_groups, num_tokens, counter + ) tokens = self._get_random_tokens_for_voucher( voucher, counter, @@ -915,7 +872,9 @@ class PaymentController(object): ) # Ask the redeemer to do the real task of redemption. - self._log.info("Redeeming random tokens for a voucher ({voucher}).", voucher=voucher) + self._log.info( + "Redeeming random tokens for a voucher ({voucher}).", voucher=voucher + ) d = bracket( lambda: setitem( self._active, diff --git a/src/_zkapauthorizer/eliot.py b/src/_zkapauthorizer/eliot.py index 1cd64008f3bfc1b1a8670d67c496660c4d8824f0..8f607d8a0e66e7605ff4d66cd5640d759bba3cdb 100644 --- a/src/_zkapauthorizer/eliot.py +++ b/src/_zkapauthorizer/eliot.py @@ -16,15 +16,9 @@ Eliot field, message, and action definitions for ZKAPAuthorizer. """ -from __future__ import ( - absolute_import, -) +from __future__ import absolute_import -from eliot import ( - Field, - MessageType, - ActionType, -) +from eliot import ActionType, Field, MessageType PRIVACYPASS_MESSAGE = Field( u"message", diff --git a/src/_zkapauthorizer/foolscap.py b/src/_zkapauthorizer/foolscap.py index b8f12425e72f3c3e80df6350f59f16a6c93bb635..20ba99fde74bfcf6f0a3f92281867887864c0d1c 100644 --- a/src/_zkapauthorizer/foolscap.py +++ b/src/_zkapauthorizer/foolscap.py @@ -17,32 +17,14 @@ Definitions related to the Foolscap-based protocol used by ZKAPAuthorizer to communicate between storage clients and servers. """ -from __future__ import ( - absolute_import, -) +from __future__ import absolute_import import attr +from allmydata.interfaces import Offset, RIStorageServer, StorageIndex +from foolscap.api import Any, Copyable, DictOf, ListOf, RemoteCopy +from foolscap.constraint import ByteStringConstraint +from foolscap.remoteinterface import RemoteInterface, RemoteMethodSchema -from foolscap.constraint import ( - ByteStringConstraint, -) -from foolscap.api import ( - Any, - DictOf, - ListOf, - Copyable, - RemoteCopy, -) -from foolscap.remoteinterface import ( - RemoteMethodSchema, - RemoteInterface, -) - -from allmydata.interfaces import ( - StorageIndex, - RIStorageServer, - Offset, -) @attr.s class ShareStat(Copyable, RemoteCopy): @@ -54,6 +36,7 @@ class ShareStat(Copyable, RemoteCopy): :ivar int lease_expiration: The POSIX timestamp of the time at which the lease on this share expires, or None if there is no lease. """ + typeToCopy = copytype = "ShareStat" # To be a RemoteCopy it must be possible to instantiate this with no @@ -134,7 +117,8 @@ def add_arguments(schema, kwargs): # arguments. modified_schema.argumentNames = ( # The new arguments - list(argName for (argName, _) in kwargs) + + list(argName for (argName, _) in kwargs) + + # The original arguments in the original order schema.argumentNames ) @@ -151,9 +135,8 @@ class RIPrivacyPassAuthorizedStorageServer(RemoteInterface): are expected to supply suitable passes and only after the passes have been validated is service provided. """ - __remote_name__ = ( - "RIPrivacyPassAuthorizedStorageServer.tahoe.privatestorage.io" - ) + + __remote_name__ = "RIPrivacyPassAuthorizedStorageServer.tahoe.privatestorage.io" get_version = RIStorageServer["get_version"] @@ -164,11 +147,11 @@ class RIPrivacyPassAuthorizedStorageServer(RemoteInterface): get_buckets = RIStorageServer["get_buckets"] def share_sizes( - storage_index_or_slot=StorageIndex, - # Notionally, ChoiceOf(None, SetOf(int, maxLength=MAX_BUCKETS)). - # However, support for such a construction appears to be - # unimplemented in Foolscap. So, instead... - sharenums=Any(), + storage_index_or_slot=StorageIndex, + # Notionally, ChoiceOf(None, SetOf(int, maxLength=MAX_BUCKETS)). + # However, support for such a construction appears to be + # unimplemented in Foolscap. So, instead... + sharenums=Any(), ): """ Get the size of the given shares in the given storage index or slot. If a @@ -177,7 +160,7 @@ class RIPrivacyPassAuthorizedStorageServer(RemoteInterface): return DictOf(int, Offset) def stat_shares( - storage_indexes_or_slots=ListOf(StorageIndex), + storage_indexes_or_slots=ListOf(StorageIndex), ): """ Get various metadata about shares in the given storage index or slot. diff --git a/src/_zkapauthorizer/lease_maintenance.py b/src/_zkapauthorizer/lease_maintenance.py index 6d15951d6411f9861d4358b6043e66342c5c38e7..8858c91b85e42017a92131d41691cf5f69fd35fa 100644 --- a/src/_zkapauthorizer/lease_maintenance.py +++ b/src/_zkapauthorizer/lease_maintenance.py @@ -17,55 +17,26 @@ This module implements a service which periodically spends ZKAPs to refresh leases on all shares reachable from a root. """ -from functools import ( - partial, -) -from datetime import ( - datetime, - timedelta, -) -from errno import ( - ENOENT, -) -import attr - -from zope.interface import ( - implementer, -) - -from aniso8601 import ( - parse_datetime, -) +from datetime import datetime, timedelta +from errno import ENOENT +from functools import partial -from twisted.internet.defer import ( - inlineCallbacks, - maybeDeferred, -) -from twisted.application.service import ( - Service, -) -from twisted.python.log import ( - err, -) - -from allmydata.interfaces import ( - IDirectoryNode, - IFilesystemNode, -) +import attr +from allmydata.interfaces import IDirectoryNode, IFilesystemNode from allmydata.util.hashutil import ( - file_renewal_secret_hash, + bucket_cancel_secret_hash, bucket_renewal_secret_hash, file_cancel_secret_hash, - bucket_cancel_secret_hash, -) - -from .controller import ( - bracket, + file_renewal_secret_hash, ) +from aniso8601 import parse_datetime +from twisted.application.service import Service +from twisted.internet.defer import inlineCallbacks, maybeDeferred +from twisted.python.log import err +from zope.interface import implementer -from .model import ( - ILeaseMaintenanceObserver, -) +from .controller import bracket +from .model import ILeaseMaintenanceObserver SERVICE_NAME = u"lease maintenance service" @@ -85,14 +56,18 @@ def visit_storage_indexes(root_nodes, visit): visited. """ if not isinstance(root_nodes, list): - raise TypeError("root_nodes must be a list, not {!r}".format( - root_nodes, - )) + raise TypeError( + "root_nodes must be a list, not {!r}".format( + root_nodes, + ) + ) for node in root_nodes: if not IFilesystemNode.providedBy(node): - raise TypeError("Root nodes must provide IFilesystemNode, {!r} does not".format( - node, - )) + raise TypeError( + "Root nodes must provide IFilesystemNode, {!r} does not".format( + node, + ) + ) stack = root_nodes[:] while stack: @@ -130,12 +105,12 @@ def iter_storage_indexes(visit_assets): @inlineCallbacks def renew_leases( - visit_assets, - storage_broker, - secret_holder, - min_lease_remaining, - get_activity_observer, - now, + visit_assets, + storage_broker, + secret_holder, + min_lease_remaining, + get_activity_observer, + now, ): """ Check the leases on a group of nodes for those which are expired or close @@ -169,9 +144,7 @@ def renew_leases( renewal_secret = secret_holder.get_renewal_secret() cancel_secret = secret_holder.get_cancel_secret() servers = list( - server.get_storage_server() - for server - in storage_broker.get_connected_servers() + server.get_storage_server() for server in storage_broker.get_connected_servers() ) for server in servers: @@ -191,13 +164,13 @@ def renew_leases( @inlineCallbacks def renew_leases_on_server( - min_lease_remaining, - renewal_secret, - cancel_secret, - storage_indexes, - server, - activity, - now, + min_lease_remaining, + renewal_secret, + cancel_secret, + storage_indexes, + server, + activity, + now, ): """ Check leases on the shares for the given storage indexes on the given @@ -318,6 +291,7 @@ class _FuzzyTimerService(Service): :ivar IReactorTime reactor: A Twisted reactor to use to schedule runs of the operation. """ + name = attr.ib() operation = attr.ib() initial_interval = attr.ib() @@ -355,12 +329,12 @@ class _FuzzyTimerService(Service): def lease_maintenance_service( - maintain_leases, - reactor, - last_run_path, - random, - interval_mean=None, - interval_range=None, + maintain_leases, + reactor, + last_run_path, + random, + interval_mean=None, + interval_range=None, ): """ Get an ``IService`` which will maintain leases on ``root_node`` and any @@ -400,6 +374,7 @@ def lease_maintenance_service( (interval_mean + halfrange).total_seconds(), ), ) + # Rather than an all-or-nothing last-run time we probably eventually want # to have a more comprehensive record of the state when we were last # interrupted. This would remove the unfortunate behavior of restarting @@ -420,7 +395,6 @@ def lease_maintenance_service( timedelta(0), ) - return _FuzzyTimerService( SERVICE_NAME, lambda: bracket( @@ -496,6 +470,7 @@ class NoopMaintenanceObserver(object): """ A lease maintenance observer that does nothing. """ + def observe(self, sizes): pass @@ -509,6 +484,7 @@ class MemoryMaintenanceObserver(object): """ A lease maintenance observer that records observations in memory. """ + observed = attr.ib(default=attr.Factory(list)) finished = attr.ib(default=False) @@ -520,12 +496,12 @@ class MemoryMaintenanceObserver(object): def maintain_leases_from_root( - get_root_nodes, - storage_broker, - secret_holder, - min_lease_remaining, - progress, - get_now, + get_root_nodes, + storage_broker, + secret_holder, + min_lease_remaining, + progress, + get_now, ): """ An operation for ``lease_maintenance_service`` which visits ``root_node`` @@ -552,6 +528,7 @@ def maintain_leases_from_root( :return: A no-argument callable to perform the maintenance. """ + def visitor(visit_assets): return renew_leases( visit_assets, diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py index 391ea1e4c40a75e28e3be5bc33d01a8a32a5bfc5..27e2fe947ebbf01d329022d72353c6b072aedca8 100644 --- a/src/_zkapauthorizer/model.py +++ b/src/_zkapauthorizer/model.py @@ -17,59 +17,26 @@ This module implements models (in the MVC sense) for the client side of the storage plugin. """ -from functools import ( - wraps, -) -from json import ( - loads, - dumps, -) -from datetime import ( - datetime, -) -from zope.interface import ( - Interface, - implementer, -) - -from sqlite3 import ( - OperationalError, - connect as _connect, -) +from datetime import datetime +from functools import wraps +from json import dumps, loads +from sqlite3 import OperationalError +from sqlite3 import connect as _connect import attr +from aniso8601 import parse_datetime as _parse_datetime +from twisted.logger import Logger +from twisted.python.filepath import FilePath +from zope.interface import Interface, implementer -from aniso8601 import ( - parse_datetime as _parse_datetime, -) -from twisted.logger import ( - Logger, -) -from twisted.python.filepath import ( - FilePath, -) - -from ._base64 import ( - urlsafe_b64decode, -) - -from .validators import ( - is_base64_encoded, - has_length, - greater_than, -) - +from ._base64 import urlsafe_b64decode +from .schema import get_schema_upgrades, get_schema_version, run_schema_upgrades from .storage_common import ( - pass_value_attribute, get_configured_pass_value, + pass_value_attribute, required_passes, ) - -from .schema import ( - get_schema_version, - get_schema_upgrades, - run_schema_upgrades, -) +from .validators import greater_than, has_length, is_base64_encoded def parse_datetime(s, **kw): @@ -89,6 +56,7 @@ class ILeaseMaintenanceObserver(Interface): An object which is interested in receiving events related to the progress of lease maintenance activity. """ + def observe(sizes): """ Observe some shares encountered during lease maintenance. @@ -106,6 +74,7 @@ class StoreOpenError(Exception): """ There was a problem opening the underlying data store. """ + def __init__(self, reason): self.reason = reason @@ -119,6 +88,7 @@ class NotEnoughTokens(Exception): CONFIG_DB_NAME = u"privatestorageio-zkapauthz-v1.sqlite3" + def open_and_initialize(path, connect=None): """ Open a SQLite3 database for use as a voucher store. @@ -204,12 +174,14 @@ def with_cursor(f): normally then the transaction will be committed. Otherwise, the transaction will be rolled back. """ + @wraps(f) def with_cursor(self, *a, **kw): with self._connection: cursor = self._connection.cursor() cursor.execute("BEGIN IMMEDIATE TRANSACTION") return f(self, cursor, *a, **kw) + return with_cursor @@ -236,6 +208,7 @@ class VoucherStore(object): :ivar now: A no-argument callable that returns the time of the call as a ``datetime`` instance. """ + _log = Logger() pass_value = pass_value_attribute() @@ -339,11 +312,7 @@ class VoucherStore(object): voucher=voucher, counter=counter, ) - tokens = list( - RandomToken(token_value) - for (token_value,) - in rows - ) + tokens = list(RandomToken(token_value) for (token_value,) in rows) else: tokens = get_tokens() self._log.info( @@ -356,17 +325,13 @@ class VoucherStore(object): """ INSERT OR IGNORE INTO [vouchers] ([number], [expected-tokens], [created]) VALUES (?, ?, ?) """, - (voucher, expected_tokens, self.now()) + (voucher, expected_tokens, self.now()), ) cursor.executemany( """ INSERT INTO [tokens] ([voucher], [counter], [text]) VALUES (?, ?, ?) """, - list( - (voucher, counter, token.token_value) - for token - in tokens - ), + list((voucher, counter, token.token_value) for token in tokens), ) return tokens @@ -387,11 +352,7 @@ class VoucherStore(object): ) refs = cursor.fetchall() - return list( - Voucher.from_row(row) - for row - in refs - ) + return list(Voucher.from_row(row) for row in refs) def _insert_unblinded_tokens(self, cursor, unblinded_tokens, group_id): """ @@ -401,11 +362,7 @@ class VoucherStore(object): """ INSERT INTO [unblinded-tokens] ([token], [redemption-group]) VALUES (?, ?) """, - list( - (token, group_id) - for token - in unblinded_tokens - ), + list((token, group_id) for token in unblinded_tokens), ) @with_cursor @@ -422,7 +379,9 @@ class VoucherStore(object): self._insert_unblinded_tokens(cursor, unblinded_tokens, group_id) @with_cursor - def insert_unblinded_tokens_for_voucher(self, cursor, voucher, public_key, unblinded_tokens, completed, spendable): + def insert_unblinded_tokens_for_voucher( + self, cursor, voucher, public_key, unblinded_tokens, completed, spendable + ): """ Store some unblinded tokens received from redemption of a voucher. @@ -442,7 +401,7 @@ class VoucherStore(object): :param bool spendable: ``True`` if it should be possible to spend the inserted tokens, ``False`` otherwise. """ - if completed: + if completed: voucher_state = u"redeemed" else: voucher_state = u"pending" @@ -488,15 +447,13 @@ class VoucherStore(object): ), ) if cursor.rowcount == 0: - raise ValueError("Cannot insert tokens for unknown voucher; add voucher first") + raise ValueError( + "Cannot insert tokens for unknown voucher; add voucher first" + ) self._insert_unblinded_tokens( cursor, - list( - t.unblinded_token - for t - in unblinded_tokens - ), + list(t.unblinded_token for t in unblinded_tokens), group_id, ) @@ -524,7 +481,7 @@ class VoucherStore(object): FROM [vouchers] WHERE [number] = ? """, - (voucher,) + (voucher,), ) rows = cursor.fetchall() if len(rows) == 0: @@ -584,11 +541,7 @@ class VoucherStore(object): """, texts, ) - return list( - UnblindedToken(t) - for (t,) - in texts - ) + return list(UnblindedToken(t) for (t,) in texts) @with_cursor def count_unblinded_tokens(self, cursor): @@ -661,11 +614,7 @@ class VoucherStore(object): """ INSERT INTO [invalid-unblinded-tokens] VALUES (?, ?) """, - list( - (token.unblinded_token, reason) - for token - in unblinded_tokens - ), + list((token.unblinded_token, reason) for token in unblinded_tokens), ) cursor.execute( """ @@ -784,6 +733,7 @@ class LeaseMaintenance(object): objects, the database row id that corresponds to the started run. This is used to make sure future updates go to the right row. """ + _pass_value = pass_value_attribute() _now = attr.ib() _connection = attr.ib() @@ -854,6 +804,7 @@ class LeaseMaintenanceActivity(object): # x = store.get_latest_lease_maintenance_activity() # xs.started, xs.passes_required, xs.finished + @attr.s(frozen=True) class UnblindedToken(object): """ @@ -867,6 +818,7 @@ class UnblindedToken(object): ``challenge_bypass_ristretto.UnblindedToken`` using that class's ``decode_base64`` method. """ + unblinded_token = attr.ib( validator=attr.validators.and_( attr.validators.instance_of(unicode), @@ -888,6 +840,7 @@ class Pass(object): text should be kept secret. If pass text is divulged to third-parties the anonymity property may be compromised. """ + preimage = attr.ib( validator=attr.validators.and_( attr.validators.instance_of(unicode), @@ -915,6 +868,7 @@ class RandomToken(object): :ivar unicode token_value: The base64-encoded representation of the random token. """ + token_value = attr.ib( validator=attr.validators.and_( attr.validators.instance_of(unicode), @@ -941,6 +895,7 @@ class Pending(object): :ivar int counter: The number of partial redemptions which have been successfully performed for the voucher. """ + counter = _counter_attribute() def should_start_redemption(self): @@ -960,6 +915,7 @@ class Redeeming(object): state is **pending** but for which there is a redemption operation in progress. """ + started = attr.ib(validator=attr.validators.instance_of(datetime)) counter = _counter_attribute() @@ -984,6 +940,7 @@ class Redeemed(object): :ivar int token_count: The number of tokens the voucher was redeemed for. """ + finished = attr.ib(validator=attr.validators.instance_of(datetime)) token_count = attr.ib(validator=attr.validators.instance_of((int, long))) @@ -1019,6 +976,7 @@ class Unpaid(object): state is **pending** but the most recent redemption attempt has failed due to lack of payment. """ + finished = attr.ib(validator=attr.validators.instance_of(datetime)) def should_start_redemption(self): @@ -1038,6 +996,7 @@ class Error(object): state is **pending** but the most recent redemption attempt has failed due to an error that is not handled by any other part of the system. """ + finished = attr.ib(validator=attr.validators.instance_of(datetime)) details = attr.ib(validator=attr.validators.instance_of(unicode)) @@ -1070,6 +1029,7 @@ class Voucher(object): an instance of ``Pending``, ``Redeeming``, ``Redeemed``, ``DoubleSpend``, ``Unpaid``, or ``Error``. """ + number = attr.ib( validator=attr.validators.and_( attr.validators.instance_of(unicode), @@ -1094,14 +1054,16 @@ class Voucher(object): state = attr.ib( default=Pending(counter=0), - validator=attr.validators.instance_of(( - Pending, - Redeeming, - Redeemed, - DoubleSpend, - Unpaid, - Error, - )), + validator=attr.validators.instance_of( + ( + Pending, + Redeeming, + Redeemed, + DoubleSpend, + Unpaid, + Error, + ) + ), ) @classmethod @@ -1140,7 +1102,6 @@ class Voucher(object): version = values.pop(u"version") return getattr(cls, "from_json_v{}".format(version))(values) - @classmethod def from_json_v1(cls, values): state_json = values[u"state"] @@ -1176,19 +1137,18 @@ class Voucher(object): return cls( number=values[u"number"], expected_tokens=values[u"expected-tokens"], - created=None if values[u"created"] is None else parse_datetime(values[u"created"]), + created=None + if values[u"created"] is None + else parse_datetime(values[u"created"]), state=state, ) - def to_json(self): return dumps(self.marshal()) - def marshal(self): return self.to_json_v1() - def to_json_v1(self): state = self.state.to_json_v1() return { diff --git a/src/_zkapauthorizer/pricecalculator.py b/src/_zkapauthorizer/pricecalculator.py index 007ec9cdf212839ddabbac9555308b29fb158483..f37e0d7e66aa8e69de13601af14f4fa56324b79a 100644 --- a/src/_zkapauthorizer/pricecalculator.py +++ b/src/_zkapauthorizer/pricecalculator.py @@ -29,10 +29,8 @@ calculator). import attr -from .storage_common import ( - required_passes, - share_size_for_data, -) +from .storage_common import required_passes, share_size_for_data + @attr.s class PriceCalculator(object): @@ -46,6 +44,7 @@ class PriceCalculator(object): :ivar int _pass_value: The bytes component of the bytes×time value of a single pass. """ + _shares_needed = attr.ib() _shares_total = attr.ib() _pass_value = attr.ib() @@ -59,15 +58,10 @@ class PriceCalculator(object): :return int: The number of ZKAPs required. """ - share_sizes = ( - share_size_for_data(self._shares_needed, size) - for size - in sizes - ) + share_sizes = (share_size_for_data(self._shares_needed, size) for size in sizes) all_required_passes = ( required_passes(self._pass_value, [share_size] * self._shares_total) - for share_size - in share_sizes + for share_size in share_sizes ) price = sum(all_required_passes, 0) return price diff --git a/src/_zkapauthorizer/private.py b/src/_zkapauthorizer/private.py index baf837206b53e067be86446bc956023908a303e1..535c00203840ce36eaa3c4e0a9c13cf6d261dd02 100644 --- a/src/_zkapauthorizer/private.py +++ b/src/_zkapauthorizer/private.py @@ -10,52 +10,7 @@ Support code for applying token-based HTTP authorization rules to a Twisted Web resource hierarchy. """ -from __future__ import ( - print_function, - unicode_literals, - absolute_import, - division, -) - -import attr - -from zope.interface import ( - implementer, -) - -from twisted.python.failure import ( - Failure, -) -from twisted.internet.defer import ( - succeed, - fail, -) -from twisted.cred.credentials import ( - ICredentials, -) -from twisted.cred.portal import ( - IRealm, - Portal, -) -from twisted.cred.checkers import ( - ANONYMOUS, -) -from twisted.cred.error import ( - UnauthorizedLogin, -) -from twisted.web.iweb import ( - ICredentialFactory, -) -from twisted.web.resource import ( - IResource, -) -from twisted.web.guard import ( - HTTPAuthSessionWrapper, -) - -from cryptography.hazmat.primitives.constant_time import ( - bytes_eq, -) +from __future__ import absolute_import, division, print_function, unicode_literals # https://github.com/twisted/nevow/issues/106 may affect this code but if so # then the hotfix Tahoe-LAFS applies should deal with it. @@ -64,10 +19,24 @@ from cryptography.hazmat.primitives.constant_time import ( # public but we do want to make sure that hotfix is applied. This seems like # an alright compromise. import allmydata.web.private as awp +import attr +from cryptography.hazmat.primitives.constant_time import bytes_eq +from twisted.cred.checkers import ANONYMOUS +from twisted.cred.credentials import ICredentials +from twisted.cred.error import UnauthorizedLogin +from twisted.cred.portal import IRealm, Portal +from twisted.internet.defer import fail, succeed +from twisted.python.failure import Failure +from twisted.web.guard import HTTPAuthSessionWrapper +from twisted.web.iweb import ICredentialFactory +from twisted.web.resource import IResource +from zope.interface import implementer + del awp SCHEME = b"tahoe-lafs" + class IToken(ICredentials): def check(auth_token): pass diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py index 1663b0c291d030e16614f894e54104f0a4dc73fe..7b81c26579e36108a2a369271841a6f355f528b2 100644 --- a/src/_zkapauthorizer/resource.py +++ b/src/_zkapauthorizer/resource.py @@ -21,63 +21,27 @@ vouchers for fresh tokens. In the future it should also allow users to read statistics about token usage. """ -from sys import ( - maxint, -) -from itertools import ( - islice, -) -from json import ( - loads, - load, - dumps, -) -from zope.interface import ( - Attribute, -) -from twisted.logger import ( - Logger, -) -from twisted.web.http import ( - BAD_REQUEST, -) -from twisted.web.server import ( - NOT_DONE_YET, -) -from twisted.web.resource import ( - IResource, - ErrorPage, - NoResource, - Resource, -) - -from . import ( - __version__ as _zkapauthorizer_version, -) - -from ._base64 import ( - urlsafe_b64decode, -) - +from itertools import islice +from json import dumps, load, loads +from sys import maxint + +from twisted.logger import Logger +from twisted.web.http import BAD_REQUEST +from twisted.web.resource import ErrorPage, IResource, NoResource, Resource +from twisted.web.server import NOT_DONE_YET +from zope.interface import Attribute + +from . import __version__ as _zkapauthorizer_version +from ._base64 import urlsafe_b64decode +from .controller import PaymentController, get_redeemer +from .pricecalculator import PriceCalculator +from .private import create_private_tree from .storage_common import ( + get_configured_allowed_public_keys, + get_configured_lease_duration, + get_configured_pass_value, get_configured_shares_needed, get_configured_shares_total, - get_configured_pass_value, - get_configured_lease_duration, - get_configured_allowed_public_keys, -) - -from .pricecalculator import ( - PriceCalculator, -) - -from .controller import ( - PaymentController, - get_redeemer, -) - -from .private import ( - create_private_tree, ) # The number of tokens to submit with a voucher redemption. @@ -88,13 +52,14 @@ class IZKAPRoot(IResource): """ The root of the resource tree of this plugin's client web presence. """ + store = Attribute("The ``VoucherStore`` used by this resource tree.") controller = Attribute("The ``PaymentController`` used by this resource tree.") def get_token_count( - plugin_name, - node_config, + plugin_name, + node_config, ): """ Retrieve the configured voucher value, in number of tokens, from the given @@ -108,18 +73,20 @@ def get_token_count( :param int default: The value to return if none is configured. """ section_name = u"storageclient.plugins.{}".format(plugin_name) - return int(node_config.get_config( - section=section_name, - option=u"default-token-count", - default=NUM_TOKENS, - )) + return int( + node_config.get_config( + section=section_name, + option=u"default-token-count", + default=NUM_TOKENS, + ) + ) def from_configuration( - node_config, - store, - redeemer=None, - clock=None, + node_config, + store, + redeemer=None, + clock=None, ): """ Instantiate the plugin root resource using data from its configuration @@ -186,9 +153,9 @@ def from_configuration( def authorizationless_resource_tree( - store, - controller, - calculate_price, + store, + controller, + calculate_price, ): """ Create the full ZKAPAuthorizer client plugin resource hierarchy with no @@ -231,6 +198,7 @@ class _CalculatePrice(Resource): """ This resource exposes a storage price calculator. """ + allowedMethods = [b"POST"] render_HEAD = render_GET = None @@ -260,43 +228,50 @@ class _CalculatePrice(Resource): body_object = loads(payload) except ValueError: request.setResponseCode(BAD_REQUEST) - return dumps({ - "error": "could not parse request body", - }) + return dumps( + { + "error": "could not parse request body", + } + ) try: version = body_object[u"version"] sizes = body_object[u"sizes"] except (TypeError, KeyError): request.setResponseCode(BAD_REQUEST) - return dumps({ - "error": "could not read `version` and `sizes` properties", - }) + return dumps( + { + "error": "could not read `version` and `sizes` properties", + } + ) if version != 1: request.setResponseCode(BAD_REQUEST) - return dumps({ - "error": "did not find required version number 1 in request", - }) - - if (not isinstance(sizes, list) or - not all( - isinstance(size, (int, long)) and size >= 0 - for size - in sizes - )): + return dumps( + { + "error": "did not find required version number 1 in request", + } + ) + + if not isinstance(sizes, list) or not all( + isinstance(size, (int, long)) and size >= 0 for size in sizes + ): request.setResponseCode(BAD_REQUEST) - return dumps({ - "error": "did not find required positive integer sizes list in request", - }) + return dumps( + { + "error": "did not find required positive integer sizes list in request", + } + ) application_json(request) price = self._price_calculator.calculate(sizes) - return dumps({ - u"price": price, - u"period": self._lease_period, - }) + return dumps( + { + u"price": price, + u"period": self._lease_period, + } + ) def wrong_content_type(request, required_type): @@ -335,11 +310,14 @@ class _ProjectVersion(Resource): """ This resource exposes the version of **ZKAPAuthorizer** itself. """ + def render_GET(self, request): application_json(request) - return dumps({ - "version": _zkapauthorizer_version, - }) + return dumps( + { + "version": _zkapauthorizer_version, + } + ) class _UnblindedTokenCollection(Resource): @@ -347,6 +325,7 @@ class _UnblindedTokenCollection(Resource): This class implements inspection of unblinded tokens. Users **GET** this resource to find out about unblinded tokens in the system. """ + _log = Logger() def __init__(self, store, controller): @@ -368,17 +347,18 @@ class _UnblindedTokenCollection(Resource): position = request.args.get(b"position", [b""])[0].decode("utf-8") - return dumps({ - u"total": len(unblinded_tokens), - u"spendable": self._store.count_unblinded_tokens(), - u"unblinded-tokens": list(islice(( - token - for token - in unblinded_tokens - if token > position - ), limit)), - u"lease-maintenance-spending": self._lease_maintenance_activity(), - }) + return dumps( + { + u"total": len(unblinded_tokens), + u"spendable": self._store.count_unblinded_tokens(), + u"unblinded-tokens": list( + islice( + (token for token in unblinded_tokens if token > position), limit + ) + ), + u"lease-maintenance-spending": self._lease_maintenance_activity(), + } + ) def render_POST(self, request): """ @@ -389,7 +369,6 @@ class _UnblindedTokenCollection(Resource): self._store.insert_unblinded_tokens(unblinded_tokens, group_id=0) return dumps({}) - def _lease_maintenance_activity(self): activity = self._store.get_latest_lease_maintenance_activity() if activity is None: @@ -400,7 +379,6 @@ class _UnblindedTokenCollection(Resource): } - class _VoucherCollection(Resource): """ This class implements redemption of vouchers. Users **PUT** such numbers @@ -408,6 +386,7 @@ class _VoucherCollection(Resource): redemption controller. Child resources of this resource can also be retrieved to monitor the status of previously submitted vouchers. """ + _log = Logger() def __init__(self, store, controller): @@ -415,7 +394,6 @@ class _VoucherCollection(Resource): self._controller = controller Resource.__init__(self) - def render_PUT(self, request): """ Record a voucher and begin attempting to redeem it. @@ -425,26 +403,31 @@ class _VoucherCollection(Resource): except Exception: return bad_request(u"json request body required").render(request) if payload.keys() != [u"voucher"]: - return bad_request(u"request object must have exactly one key: 'voucher'").render(request) + return bad_request( + u"request object must have exactly one key: 'voucher'" + ).render(request) voucher = payload[u"voucher"] if not is_syntactic_voucher(voucher): - return bad_request(u"submitted voucher is syntactically invalid").render(request) + return bad_request(u"submitted voucher is syntactically invalid").render( + request + ) - self._log.info("Accepting a voucher ({voucher}) for redemption.", voucher=voucher) + self._log.info( + "Accepting a voucher ({voucher}) for redemption.", voucher=voucher + ) self._controller.redeem(voucher) return b"" - def render_GET(self, request): application_json(request) - return dumps({ - u"vouchers": list( - self._controller.incorporate_transient_state(voucher).marshal() - for voucher - in self._store.list() - ), - }) - + return dumps( + { + u"vouchers": list( + self._controller.incorporate_transient_state(voucher).marshal() + for voucher in self._store.list() + ), + } + ) def getChild(self, segment, request): voucher = segment.decode("utf-8") @@ -483,6 +466,7 @@ class VoucherView(Resource): """ This class implements a view for a ``Voucher`` instance. """ + def __init__(self, voucher): """ :param Voucher reference: The model object for which to provide a @@ -491,7 +475,6 @@ class VoucherView(Resource): self._voucher = voucher Resource.__init__(self) - def render_GET(self, request): application_json(request) return self._voucher.to_json() @@ -503,5 +486,7 @@ def bad_request(reason=u"Bad Request"): REQUEST** response. """ return ErrorPage( - BAD_REQUEST, b"Bad Request", reason.encode("utf-8"), + BAD_REQUEST, + b"Bad Request", + reason.encode("utf-8"), ) diff --git a/src/_zkapauthorizer/schema.py b/src/_zkapauthorizer/schema.py index 27890a32fcebc15cfebae08580e7daf5d33f69cc..9fe72b695f76f3c3d7bed9628499f45be4e98f8d 100644 --- a/src/_zkapauthorizer/schema.py +++ b/src/_zkapauthorizer/schema.py @@ -12,14 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import ( - unicode_literals, -) +from __future__ import unicode_literals """ This module defines the database schema used by the model interface. """ + def get_schema_version(cursor): cursor.execute( """ @@ -63,12 +62,10 @@ def run_schema_upgrades(upgrades, cursor): cursor.execute(upgrade) -_INCREMENT_VERSION = ( - """ +_INCREMENT_VERSION = """ UPDATE [version] SET [version] = [version] + 1 """ -) # A mapping from old schema versions to lists of unicode strings of SQL to # execute against that version of the schema to create the successor schema. @@ -125,7 +122,6 @@ _UPGRADES = { ) """, ], - 1: [ """ -- Incorrectly track a single public-key for all. Later version of @@ -133,14 +129,12 @@ _UPGRADES = { ALTER TABLE [vouchers] ADD COLUMN [public-key] text """, ], - 2: [ """ -- Keep track of progress through redemption of each voucher. ALTER TABLE [vouchers] ADD COLUMN [counter] integer DEFAULT 0 """, ], - 3: [ """ -- Reference to the counter these tokens go with. @@ -158,7 +152,6 @@ _UPGRADES = { ALTER TABLE [vouchers] ADD COLUMN [expected-tokens] integer NOT NULL DEFAULT 32768 """, ], - 4: [ """ CREATE TABLE [invalid-unblinded-tokens] ( @@ -169,7 +162,6 @@ _UPGRADES = { ) """, ], - 5: [ """ -- Create a table where rows represent a single group of unblinded @@ -190,7 +182,6 @@ _UPGRADES = { [public-key] text ) """, - """ -- Create one redemption group for every existing, redeemed voucher. -- These tokens were probably *not* all redeemed in one group but @@ -199,14 +190,12 @@ _UPGRADES = { INSERT INTO [redemption-groups] ([voucher], [public-key], [spendable]) SELECT DISTINCT([number]), [public-key], 1 FROM [vouchers] WHERE [state] = "redeemed" """, - """ -- Give each voucher a count of "sequestered" tokens. Currently, -- these are unspendable tokens that were issued using a disallowed -- public key. ALTER TABLE [vouchers] ADD COLUMN [sequestered-count] integer NOT NULL DEFAULT 0 """, - """ -- Give each unblinded token a reference to the [redemption-groups] -- table identifying the group that token arrived with. This lets us diff --git a/src/_zkapauthorizer/spending.py b/src/_zkapauthorizer/spending.py index 20f0f775f17622f868e19790a35f7725b53f4759..19b96ecb1f597e0ddbe26e1840853c6af7b0ef64 100644 --- a/src/_zkapauthorizer/spending.py +++ b/src/_zkapauthorizer/spending.py @@ -16,25 +16,17 @@ A module for logic controlling the manner in which ZKAPs are spent. """ -from zope.interface import ( - Interface, - Attribute, - implementer, -) - import attr +from zope.interface import Attribute, Interface, implementer + +from .eliot import GET_PASSES, INVALID_PASSES, RESET_PASSES, SPENT_PASSES -from .eliot import ( - GET_PASSES, - SPENT_PASSES, - INVALID_PASSES, - RESET_PASSES, -) class IPassGroup(Interface): """ A group of passed meant to be spent together. """ + passes = Attribute(":ivar list[Pass] passes: The passes themselves.") def split(select_indices): @@ -91,6 +83,7 @@ class IPassFactory(Interface): """ An object which can create passes. """ + def get(message, num_passes): """ :param unicode message: A request-binding message for the resulting passes. @@ -115,25 +108,18 @@ class PassGroup(object): :ivar list[Pass] passes: The passes of which this group consists. """ + _message = attr.ib() _factory = attr.ib() _tokens = attr.ib() @property def passes(self): - return list( - pass_ - for (unblinded_token, pass_) - in self._tokens - ) + return list(pass_ for (unblinded_token, pass_) in self._tokens) @property def unblinded_tokens(self): - return list( - unblinded_token - for (unblinded_token, pass_) - in self._tokens - ) + return list(unblinded_token for (unblinded_token, pass_) in self._tokens) def split(self, select_indices): selected = [] @@ -171,6 +157,7 @@ class SpendingController(object): A ``SpendingController`` gives out ZKAPs and arranges for re-spend attempts when necessary. """ + get_unblinded_tokens = attr.ib() discard_unblinded_tokens = attr.ib() invalidate_unblinded_tokens = attr.ib() diff --git a/src/_zkapauthorizer/storage_common.py b/src/_zkapauthorizer/storage_common.py index 0709e4152cd6773d0c8a39b9055df3202f15bab2..9af2a922fda5ce161a02294438842a3f3f27e5c3 100644 --- a/src/_zkapauthorizer/storage_common.py +++ b/src/_zkapauthorizer/storage_common.py @@ -16,27 +16,16 @@ Functionality shared between the storage client and server. """ -from __future__ import ( - division, -) +from __future__ import division -from base64 import ( - b64encode, -) +from base64 import b64encode import attr +from pyutil.mathutil import div_ceil -from .validators import ( - greater_than, -) +from .eliot import MUTABLE_PASSES_REQUIRED +from .validators import greater_than -from .eliot import ( - MUTABLE_PASSES_REQUIRED, -) - -from pyutil.mathutil import ( - div_ceil, -) @attr.s(frozen=True, str=True) class MorePassesRequired(Exception): @@ -53,6 +42,7 @@ class MorePassesRequired(Exception): :ivar set[int] signature_check_failed: Indices into the supplied list of passes indicating passes which failed the signature check. """ + valid_count = attr.ib(validator=attr.validators.instance_of((int, long))) required_count = attr.ib(validator=attr.validators.instance_of((int, long))) signature_check_failed = attr.ib(converter=frozenset) @@ -64,18 +54,23 @@ def _message_maker(label): label=label, storage_index=b64encode(storage_index), ) + return make_message + # Functions to construct the PrivacyPass request-binding message for pass # construction for different Tahoe-LAFS storage operations. allocate_buckets_message = _message_maker(u"allocate_buckets") add_lease_message = _message_maker(u"add_lease") -slot_testv_and_readv_and_writev_message = _message_maker(u"slot_testv_and_readv_and_writev") +slot_testv_and_readv_and_writev_message = _message_maker( + u"slot_testv_and_readv_and_writev" +) # The number of bytes we're willing to store for a lease period for each pass # submitted. BYTES_PER_PASS = 1024 * 1024 + def get_configured_shares_needed(node_config): """ Determine the configured-specified value of "needed" shares (``k``). @@ -83,11 +78,13 @@ def get_configured_shares_needed(node_config): If no value is explicitly configured, the Tahoe-LAFS default (as best as we know it) is returned. """ - return int(node_config.get_config( - section=u"client", - option=u"shares.needed", - default=3, - )) + return int( + node_config.get_config( + section=u"client", + option=u"shares.needed", + default=3, + ) + ) def get_configured_shares_total(node_config): @@ -97,11 +94,13 @@ def get_configured_shares_total(node_config): If no value is explicitly configured, the Tahoe-LAFS default (as best as we know it) is returned. """ - return int(node_config.get_config( - section=u"client", - option=u"shares.total", - default=10, - )) + return int( + node_config.get_config( + section=u"client", + option=u"shares.total", + default=10, + ) + ) def get_configured_pass_value(node_config): @@ -113,11 +112,13 @@ def get_configured_pass_value(node_config): client section. """ section_name = u"storageclient.plugins.privatestorageio-zkapauthz-v1" - return int(node_config.get_config( - section=section_name, - option=u"pass-value", - default=BYTES_PER_PASS, - )) + return int( + node_config.get_config( + section=section_name, + option=u"pass-value", + default=BYTES_PER_PASS, + ) + ) def get_configured_lease_duration(node_config): @@ -136,10 +137,14 @@ def get_configured_allowed_public_keys(node_config): Read the set of allowed issuer public keys from the given configuration. """ section_name = u"storageclient.plugins.privatestorageio-zkapauthz-v1" - return set(node_config.get_config( - section=section_name, - option=u"allowed-public-keys", - ).strip().split(",")) + return set( + node_config.get_config( + section=section_name, + option=u"allowed-public-keys", + ) + .strip() + .split(",") + ) def required_passes(bytes_per_pass, share_sizes): @@ -194,8 +199,7 @@ def has_writes(tw_vectors): """ return any( data or (new_length is not None) - for (test, data, new_length) - in tw_vectors.values() + for (test, data, new_length) in tw_vectors.values() ) @@ -207,10 +211,7 @@ def get_sharenums(tw_vectors): :return set[int]: The share numbers which the given test/write vectors would write to. """ return set( - sharenum - for (sharenum, (test, data, new_length)) - in tw_vectors.items() - if data + sharenum for (sharenum, (test, data, new_length)) in tw_vectors.items() if data ) @@ -224,8 +225,7 @@ def get_allocated_size(tw_vectors): return max( list( max(offset + len(s) for (offset, s) in data) - for (sharenum, (test, data, new_length)) - in tw_vectors.items() + for (sharenum, (test, data, new_length)) in tw_vectors.items() if data ), ) @@ -241,11 +241,9 @@ def get_implied_data_length(data_vector, new_length): :return int: The amount of data, in bytes, implied by a data vector and a size. """ - data_based_size = max( - offset + len(data) - for (offset, data) - in data_vector - ) if data_vector else 0 + data_based_size = ( + max(offset + len(data) for (offset, data) in data_vector) if data_vector else 0 + ) if new_length is None: return data_based_size # new_length is only allowed to truncate, not expand. @@ -266,8 +264,7 @@ def get_required_new_passes_for_mutable_write(pass_value, current_sizes, tw_vect new_sizes = current_sizes.copy() size_updates = { sharenum: get_implied_data_length(data_vector, new_length) - for (sharenum, (_, data_vector, new_length)) - in tw_vectors.items() + for (sharenum, (_, data_vector, new_length)) in tw_vectors.items() } for sharenum, size in size_updates.items(): if size > new_sizes.get(sharenum, 0): @@ -289,25 +286,21 @@ def get_required_new_passes_for_mutable_write(pass_value, current_sizes, tw_vect ) return required_new_passes + def summarize(tw_vectors): return { sharenum: ( list( (offset, length, operator, len(specimen)) - for (offset, length, operator, specimen) - in test_vector - ), - list( - (offset, len(data)) - for (offset, data) - in data_vectors + for (offset, length, operator, specimen) in test_vector ), + list((offset, len(data)) for (offset, data) in data_vectors), new_length, ) - for (sharenum, (test_vector, data_vectors, new_length)) - in tw_vectors.items() + for (sharenum, (test_vector, data_vectors, new_length)) in tw_vectors.items() } + def pass_value_attribute(): """ Define an attribute for an attrs-based object which can hold a pass value. diff --git a/src/_zkapauthorizer/tests/__init__.py b/src/_zkapauthorizer/tests/__init__.py index 102647a022c45553eb27ea9da5dfd0e433a11941..313d67dc92c15224f147dd97e1a229a127bb95df 100644 --- a/src/_zkapauthorizer/tests/__init__.py +++ b/src/_zkapauthorizer/tests/__init__.py @@ -16,6 +16,7 @@ The automated unit test suite. """ + def _configure_hypothesis(): """ Select define Hypothesis profiles and select one based on environment @@ -23,10 +24,7 @@ def _configure_hypothesis(): """ from os import environ - from hypothesis import ( - HealthCheck, - settings, - ) + from hypothesis import HealthCheck, settings base = dict( suppress_health_check=[ @@ -41,10 +39,7 @@ def _configure_hypothesis(): deadline=None, ) - settings.register_profile( - "default", - **base - ) + settings.register_profile("default", **base) settings.register_profile( "ci", @@ -70,4 +65,5 @@ def _configure_hypothesis(): settings.load_profile(profile_name) print("Loaded profile {}".format(profile_name)) + _configure_hypothesis() diff --git a/src/_zkapauthorizer/tests/_exception.py b/src/_zkapauthorizer/tests/_exception.py index 5e8bc8f5b21c29ebba4e93528a417761ff3fcf1d..35dd0b73af4cb3e71fc650082bae5f2df8706a45 100644 --- a/src/_zkapauthorizer/tests/_exception.py +++ b/src/_zkapauthorizer/tests/_exception.py @@ -15,20 +15,15 @@ # limitations under the License. __all__ = [ - 'MatchesExceptionType', - 'Raises', - 'raises', - ] + "MatchesExceptionType", + "Raises", + "raises", +] import sys -from testtools.matchers import ( - Matcher, - Mismatch, -) -from testtools.content import ( - TracebackContent, -) +from testtools.content import TracebackContent +from testtools.matchers import Matcher, Mismatch def _is_exception(exc): @@ -55,7 +50,7 @@ class MatchesExceptionType(Matcher): def match(self, other): if type(other) != tuple: - return Mismatch('{!r} is not an exc_info tuple'.format(other)) + return Mismatch("{!r} is not an exc_info tuple".format(other)) expected_class = self.expected etype, evalue, etb = other if not issubclass(etype, expected_class): @@ -94,7 +89,7 @@ class Raises(Matcher): def match(self, matchee): try: result = matchee() - return Mismatch('%r returned %r' % (matchee, result)) + return Mismatch("%r returned %r" % (matchee, result)) # Catch all exceptions: Raises() should be able to match a # KeyboardInterrupt or SystemExit. except: @@ -113,7 +108,7 @@ class Raises(Matcher): return None def __str__(self): - return 'Raises()' + return "Raises()" def raises(exception_type): diff --git a/src/_zkapauthorizer/tests/common.py b/src/_zkapauthorizer/tests/common.py index 39aa9119e10c22670c2948b4f53a7dbb22dc4f6e..3a362a69d7b7ea6b7336890cdc6560fab6341a14 100644 --- a/src/_zkapauthorizer/tests/common.py +++ b/src/_zkapauthorizer/tests/common.py @@ -37,5 +37,7 @@ def _skipper(reason): def wrapper(f): def skipIt(self, *a, **kw): self.skipTest(reason) + return skipIt + return wrapper diff --git a/src/_zkapauthorizer/tests/eliot.py b/src/_zkapauthorizer/tests/eliot.py index 710737d948cc4e069d12c265277e95dee569e133..ba010cf2b51b709f19abfa18e1e0023fa1a38418 100644 --- a/src/_zkapauthorizer/tests/eliot.py +++ b/src/_zkapauthorizer/tests/eliot.py @@ -16,26 +16,14 @@ Eliot testing helpers. """ -from __future__ import ( - absolute_import, -) - -from functools import ( - wraps, -) - -from unittest import ( - SkipTest, -) - -from eliot import ( - MemoryLogger, -) - -from eliot.testing import ( - swap_logger, - check_for_errors, -) +from __future__ import absolute_import + +from functools import wraps +from unittest import SkipTest + +from eliot import MemoryLogger +from eliot.testing import check_for_errors, swap_logger + # validate_logging and capture_logging copied from Eliot around 1.11. We # can't upgrade past 1.7 because we're not Python 3 compatible. diff --git a/src/_zkapauthorizer/tests/fixtures.py b/src/_zkapauthorizer/tests/fixtures.py index 7f2246a2e15a605895730dd0fff3dfe3ea3978f1..3a4e4155fb8cbf4f19c36bd6f0e29c22b2ad79bc 100644 --- a/src/_zkapauthorizer/tests/fixtures.py +++ b/src/_zkapauthorizer/tests/fixtures.py @@ -16,41 +16,20 @@ Common fixtures to let the test suite focus on application logic. """ -from __future__ import ( - absolute_import, -) +from __future__ import absolute_import -from base64 import ( - b64encode, -) +from base64 import b64encode import attr - -from fixtures import ( - Fixture, - TempDir, -) - -from twisted.python.filepath import ( - FilePath, -) -from twisted.internet.task import ( - Clock, -) from allmydata import __version__ as allmydata_version -from allmydata.storage.server import ( - StorageServer, -) - -from ..model import ( - VoucherStore, - open_and_initialize, - memory_connect, -) -from ..controller import ( - DummyRedeemer, - PaymentController, -) +from allmydata.storage.server import StorageServer +from fixtures import Fixture, TempDir +from twisted.internet.task import Clock +from twisted.python.filepath import FilePath + +from ..controller import DummyRedeemer, PaymentController +from ..model import VoucherStore, memory_connect, open_and_initialize + @attr.s class AnonymousStorageServer(Fixture): @@ -67,6 +46,7 @@ class AnonymousStorageServer(Fixture): :ivar twisted.internet.task.Clock clock: The ``IReactorTime`` provider to supply to ``StorageServer`` for its time-checking needs. """ + clock = attr.ib() def _setUp(self): @@ -82,9 +62,7 @@ class AnonymousStorageServer(Fixture): timeargs = {} self.storage_server = StorageServer( - self.tempdir.asBytesMode().path, - b"x" * 20, - **timeargs + self.tempdir.asBytesMode().path, b"x" * 20, **timeargs ) @@ -100,6 +78,7 @@ class TemporaryVoucherStore(Fixture): :ivar store: A newly created temporary store. """ + get_config = attr.ib() get_now = attr.ib() @@ -122,6 +101,7 @@ class ConfiglessMemoryVoucherStore(Fixture): This is like ``TemporaryVoucherStore`` but faster because it skips the Tahoe-LAFS parts. """ + get_now = attr.ib() _public_key = attr.ib(default=b64encode(b"A" * 32).decode("utf-8")) redeemer = attr.ib(default=None, init=False) diff --git a/src/_zkapauthorizer/tests/foolscap.py b/src/_zkapauthorizer/tests/foolscap.py index 35716998d69bb32c5066f244e285aececd0bf6ba..3a984bea163fd4c567812556f8229508c0cb8a2d 100644 --- a/src/_zkapauthorizer/tests/foolscap.py +++ b/src/_zkapauthorizer/tests/foolscap.py @@ -16,35 +16,15 @@ Testing helpers related to Foolscap. """ -from __future__ import ( - absolute_import, -) - -from zope.interface import ( - implementer, -) +from __future__ import absolute_import import attr +from allmydata.interfaces import RIStorageServer +from foolscap.api import Any, Copyable, Referenceable, RemoteInterface +from foolscap.copyable import CopyableSlicer, ICopyable +from twisted.internet.defer import fail, succeed +from zope.interface import implementer -from twisted.internet.defer import ( - succeed, - fail, -) - -from foolscap.api import ( - RemoteInterface, - Referenceable, - Copyable, - Any, -) -from foolscap.copyable import ( - ICopyable, - CopyableSlicer, -) - -from allmydata.interfaces import ( - RIStorageServer, -) class RIStub(RemoteInterface): pass @@ -54,6 +34,7 @@ class RIEcho(RemoteInterface): def echo(argument=Any()): return Any() + @implementer(RIStorageServer) class StubStorageServer(object): pass @@ -85,11 +66,13 @@ class DummyReferenceable(object): def doRemoteCall(self, *a, **kw): return None + @attr.s class LocalTracker(object): """ Pretend to be a tracker for a ``LocalRemote``. """ + interface = attr.ib() interfaceName = attr.ib(default=None) @@ -115,6 +98,7 @@ class LocalRemote(object): :ivar foolscap.ipb.IReferenceable _referenceable: The object to which this provides a simulated remote interface. """ + _referenceable = attr.ib() check_args = attr.ib(default=True) tracker = attr.ib(default=None) diff --git a/src/_zkapauthorizer/tests/json.py b/src/_zkapauthorizer/tests/json.py index e96cc74bbd830ccf7af0ad52882143bd9824594c..b8aa7c74548cf3f9a744dcf0cd44bc03c7afb689 100644 --- a/src/_zkapauthorizer/tests/json.py +++ b/src/_zkapauthorizer/tests/json.py @@ -16,13 +16,10 @@ A better JSON module. """ -from __future__ import ( - absolute_import, -) +from __future__ import absolute_import + +from json import loads as _loads -from json import ( - loads as _loads, -) def loads(data): """ diff --git a/src/_zkapauthorizer/tests/matchers.py b/src/_zkapauthorizer/tests/matchers.py index ea3343143e2d75ccf4c62d2f3c793265d64b6575..4bfe7362bcfa96eb3198cb7ef6cc0ca3b0e9ea90 100644 --- a/src/_zkapauthorizer/tests/matchers.py +++ b/src/_zkapauthorizer/tests/matchers.py @@ -25,43 +25,35 @@ __all__ = [ "leases_current", ] -from datetime import ( - datetime, -) +from datetime import datetime import attr - from testtools.matchers import ( - Matcher, - Mismatch, - ContainsDict, + AfterPreprocessing, + AllMatch, Always, + ContainsDict, + Equals, + GreaterThan, + LessThan, + Matcher, MatchesAll, MatchesAny, MatchesStructure, - GreaterThan, - LessThan, - Equals, - AfterPreprocessing, - AllMatch, -) -from testtools.twistedsupport import ( - succeeded, + Mismatch, ) +from testtools.twistedsupport import succeeded +from treq import content -from treq import ( - content, -) +from ._exception import raises -from ._exception import ( - raises, -) @attr.s class Provides(object): """ Match objects that provide all of a list of Zope Interface interfaces. """ + interfaces = attr.ib(validator=attr.validators.instance_of(list)) def match(self, obj): @@ -70,9 +62,12 @@ class Provides(object): if not iface.providedBy(obj): missing.add(iface) if missing: - return Mismatch("{} does not provide expected {}".format( - obj, ", ".join(str(iface) for iface in missing), - )) + return Mismatch( + "{} does not provide expected {}".format( + obj, + ", ".join(str(iface) for iface in missing), + ) + ) def matches_version_dictionary(): @@ -81,13 +76,14 @@ def matches_version_dictionary(): ``RIStorageServer.get_version`` which is also the dictionary returned by our own ``RIPrivacyPassAuthorizedStorageServer.get_version``. """ - return ContainsDict({ - # It has these two top-level keys, at least. Try not to be too - # fragile by asserting much more than that they are present. - b'application-version': Always(), - b'http://allmydata.org/tahoe/protocols/storage/v1': Always(), - }) - + return ContainsDict( + { + # It has these two top-level keys, at least. Try not to be too + # fragile by asserting much more than that they are present. + b"application-version": Always(), + b"http://allmydata.org/tahoe/protocols/storage/v1": Always(), + } + ) def returns(matcher): @@ -98,16 +94,13 @@ def returns(matcher): return _Returns(matcher) - class _Returns(Matcher): def __init__(self, result_matcher): self.result_matcher = result_matcher - def match(self, matchee): return self.result_matcher.match(matchee()) - def __str__(self): return "Returns({})".format(self.result_matcher) @@ -147,8 +140,7 @@ def leases_current(relevant_storage_indexes, now, min_lease_remaining): # visited and maintained. lambda storage_server: list( stat - for (storage_index, stat) - in storage_server.buckets.items() + for (storage_index, stat) in storage_server.buckets.items() if storage_index in relevant_storage_indexes ), AllMatch( @@ -157,7 +149,9 @@ def leases_current(relevant_storage_indexes, now, min_lease_remaining): # 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), + lambda share_stat: datetime.utcfromtimestamp( + share_stat.lease_expiration + ), GreaterThan(now + min_lease_remaining), ), ), @@ -184,7 +178,9 @@ def odd(): ) -def matches_response(code_matcher=Always(), headers_matcher=Always(), body_matcher=Always()): +def matches_response( + code_matcher=Always(), headers_matcher=Always(), body_matcher=Always() +): """ Match a Treq response object with certain code and body. diff --git a/src/_zkapauthorizer/tests/privacypass.py b/src/_zkapauthorizer/tests/privacypass.py index fcc3c6700472c03369d07927a92b408d48415d92..a3820b0924ec3f6fa812141dd004c18599fbbc47 100644 --- a/src/_zkapauthorizer/tests/privacypass.py +++ b/src/_zkapauthorizer/tests/privacypass.py @@ -16,14 +16,10 @@ Ristretto-flavored PrivacyPass helpers for the test suite. """ -from __future__ import ( - absolute_import, -) +from __future__ import absolute_import + +from challenge_bypass_ristretto import BatchDLEQProof, PublicKey -from challenge_bypass_ristretto import ( - BatchDLEQProof, - PublicKey, -) def make_passes(signing_key, for_message, random_tokens): """ @@ -41,15 +37,9 @@ def make_passes(signing_key, for_message, random_tokens): :return list[unicode]: The privacy passes. The returned list has one element for each element of ``random_tokens``. """ - blinded_tokens = list( - token.blind() - for token - in random_tokens - ) + blinded_tokens = list(token.blind() for token in random_tokens) signatures = list( - signing_key.sign(blinded_token) - for blinded_token - in blinded_tokens + signing_key.sign(blinded_token) for blinded_token in blinded_tokens ) proof = BatchDLEQProof.create( signing_key, @@ -63,26 +53,21 @@ def make_passes(signing_key, for_message, random_tokens): PublicKey.from_signing_key(signing_key), ) preimages = list( - unblinded_signature.preimage() - for unblinded_signature - in unblinded_signatures + unblinded_signature.preimage() for unblinded_signature in unblinded_signatures ) verification_keys = list( unblinded_signature.derive_verification_key_sha512() - for unblinded_signature - in unblinded_signatures + for unblinded_signature in unblinded_signatures ) message_signatures = list( verification_key.sign_sha512(for_message.encode("utf-8")) - for verification_key - in verification_keys + for verification_key in verification_keys ) passes = list( u"{} {}".format( preimage.encode_base64().decode("ascii"), signature.encode_base64().decode("ascii"), ).encode("ascii") - for (preimage, signature) - in zip(preimages, message_signatures) + for (preimage, signature) in zip(preimages, message_signatures) ) return passes diff --git a/src/_zkapauthorizer/tests/storage_common.py b/src/_zkapauthorizer/tests/storage_common.py index 5141dd58cd64ea5bf126a1364e8e2d13f8f942dd..c28cff6a4aceee2f6059e3fae735b0926b0bb4f6 100644 --- a/src/_zkapauthorizer/tests/storage_common.py +++ b/src/_zkapauthorizer/tests/storage_common.py @@ -16,57 +16,25 @@ ``allmydata.storage``-related helpers shared across the test suite. """ -from functools import ( - partial, -) - -from os import ( - SEEK_CUR, -) -from struct import ( - pack, -) - -from itertools import ( - islice, -) +from functools import partial +from itertools import islice +from os import SEEK_CUR +from struct import pack import attr +from challenge_bypass_ristretto import RandomToken +from twisted.python.filepath import FilePath +from zope.interface import implementer -from zope.interface import ( - implementer, -) - -from twisted.python.filepath import ( - FilePath, -) - -from challenge_bypass_ristretto import ( - RandomToken, -) - -from .strategies import ( - # Not really a strategy... - bytes_for_share, -) - -from .privacypass import ( - make_passes, -) - -from ..model import ( - NotEnoughTokens, - Pass, -) - -from ..spending import ( - IPassFactory, - PassGroup, -) +from ..model import NotEnoughTokens, Pass +from ..spending import IPassFactory, PassGroup +from .privacypass import make_passes +from .strategies import bytes_for_share # Not really a strategy... # Hard-coded in Tahoe-LAFS LEASE_INTERVAL = 60 * 60 * 24 * 31 + def cleanup_storage_server(storage_server): """ Delete all of the shares held by the given storage server. @@ -85,13 +53,13 @@ def cleanup_storage_server(storage_server): def write_toy_shares( - storage_server, - storage_index, - renew_secret, - cancel_secret, - sharenums, - size, - canary, + storage_server, + storage_index, + renew_secret, + cancel_secret, + sharenums, + size, + canary, ): """ Write some immutable shares to the given storage server. @@ -161,8 +129,7 @@ def whitebox_write_sparse_share(sharepath, version, size, leases, now): # expiration timestamp int(now + LEASE_INTERVAL), ) - for renew - in leases + for renew in leases ), ) @@ -175,11 +142,13 @@ def integer_passes(limit): passes. Successive calls to the function return unique pass values. """ counter = iter(range(limit)) + def get_passes(message, num_passes): result = list(islice(counter, num_passes)) if len(result) < num_passes: raise NotEnoughTokens() return result + return get_passes @@ -196,8 +165,7 @@ def get_passes(message, count, signing_key): """ return list( Pass(*pass_.split(u" ")) - for pass_ - in make_passes( + for pass_ in make_passes( signing_key, message, list(RandomToken.create() for n in range(count)), @@ -252,6 +220,7 @@ class _PassFactory(object): :ivar list[int] returned: A list of passes which were given out but then returned via ``IPassGroup.reset``. """ + _get_passes = attr.ib() returned = attr.ib(default=attr.Factory(list), init=False) @@ -291,7 +260,9 @@ class _PassFactory(object): def _mark_invalid(self, reason, passes): for p in passes: if p not in self.in_use: - raise ValueError("Pass {} cannot be invalid, it is not in use.".format(p)) + raise ValueError( + "Pass {} cannot be invalid, it is not in use.".format(p) + ) self.invalid.update(dict.fromkeys(passes, reason)) self.in_use.difference_update(passes) diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py index 4fe4df3f2f0b58fc860651a7ff9c4ddc011f0688..567c94405e6aac2009255ead776784e22ab95bad 100644 --- a/src/_zkapauthorizer/tests/strategies.py +++ b/src/_zkapauthorizer/tests/strategies.py @@ -16,77 +16,48 @@ Hypothesis strategies for property testing. """ -from base64 import ( - b64encode, - urlsafe_b64encode, -) -from datetime import ( - datetime, -) -from urllib import ( - quote, -) +from base64 import b64encode, urlsafe_b64encode +from datetime import datetime +from urllib import quote import attr - -from zope.interface import ( - implementer, -) - +from allmydata.client import config_from_string +from allmydata.interfaces import HASH_SIZE, IDirectoryNode, IFilesystemNode from hypothesis.strategies import ( - one_of, - sampled_from, - just, - none, binary, + builds, characters, - text, - integers, - sets, - lists, - tuples, + datetimes, dictionaries, fixed_dictionaries, - builds, - datetimes, + integers, + just, + lists, + none, + one_of, recursive, + sampled_from, + sets, + text, + tuples, ) +from twisted.internet.defer import succeed +from twisted.internet.task import Clock +from twisted.web.test.requesthelper import DummyRequest +from zope.interface import implementer -from twisted.internet.defer import ( - succeed, -) -from twisted.internet.task import ( - Clock, -) -from twisted.web.test.requesthelper import ( - DummyRequest, -) - -from allmydata.interfaces import ( - IFilesystemNode, - IDirectoryNode, - HASH_SIZE, -) - -from allmydata.client import ( - config_from_string, -) - +from ..configutil import config_string_from_sections from ..model import ( + DoubleSpend, + Error, Pass, + Pending, RandomToken, + Redeemed, + Redeeming, UnblindedToken, - Voucher, - Pending, - DoubleSpend, Unpaid, - Error, - Redeeming, - Redeemed, -) - -from ..configutil import ( - config_string_from_sections, + Voucher, ) # Sizes informed by @@ -101,6 +72,7 @@ _UNBLINDED_TOKEN_LENGTH = 96 # The length of a `VerificationSignature`, in bytes. _VERIFICATION_SIGNATURE_LENGTH = 64 + def tahoe_config_texts(storage_client_plugins, shares): """ Build the text of complete Tahoe-LAFS configurations for a node. @@ -113,6 +85,7 @@ def tahoe_config_texts(storage_client_plugins, shares): may be an integer or None to leave it unconfigured (and rely on the default). """ + def merge_shares(shares, the_rest): for (k, v) in zip(("needed", "happy", "total"), shares): if v is not None: @@ -138,8 +111,7 @@ def tahoe_config_texts(storage_client_plugins, shares): fixed_dictionaries( { "storageclient.plugins.{}".format(name): configs - for (name, configs) - in storage_client_plugins.items() + for (name, configs) in storage_client_plugins.items() }, ), fixed_dictionaries( @@ -198,13 +170,17 @@ def dummy_ristretto_keys(): They're not really because they're entirely random rather than points on the curve. """ - return binary( - min_size=32, - max_size=32, - ).map( - b64encode, - ).map( - lambda bs: bs.decode("ascii"), + return ( + binary( + min_size=32, + max_size=32, + ) + .map( + b64encode, + ) + .map( + lambda bs: bs.decode("ascii"), + ) ) @@ -216,17 +192,22 @@ def server_configurations(signing_key_path): **ristretto-signing-key-path** item. """ return one_of( - fixed_dictionaries({ - u"pass-value": - # The configuration is ini so everything is always a byte string! - integers(min_value=1).map(bytes), - }), + fixed_dictionaries( + { + u"pass-value": + # The configuration is ini so everything is always a byte string! + integers(min_value=1).map(bytes), + } + ), just({}), ).map( - lambda config: config.update({ - u"ristretto-issuer-root-url": u"https://issuer.example.invalid/", - u"ristretto-signing-key-path": signing_key_path.path, - }) or config, + lambda config: config.update( + { + u"ristretto-issuer-root-url": u"https://issuer.example.invalid/", + u"ristretto-signing-key-path": signing_key_path.path, + } + ) + or config, ) @@ -274,26 +255,34 @@ def client_ristrettoredeemer_configurations(): """ Build Ristretto-using configuration values for the client-side plugin. """ - return zkapauthz_configuration(just({ - u"ristretto-issuer-root-url": u"https://issuer.example.invalid/", - u"redeemer": u"ristretto", - })) + return zkapauthz_configuration( + just( + { + u"ristretto-issuer-root-url": u"https://issuer.example.invalid/", + u"redeemer": u"ristretto", + } + ) + ) def client_dummyredeemer_configurations(): """ Build DummyRedeemer-using configuration values for the client-side plugin. """ + def share_a_key(allowed_keys): return zkapauthz_configuration( - just({ - u"redeemer": u"dummy", - # Pick out one of the allowed public keys so that the dummy - # appears to produce usable tokens. - u"issuer-public-key": next(iter(allowed_keys)), - }), + just( + { + u"redeemer": u"dummy", + # Pick out one of the allowed public keys so that the dummy + # appears to produce usable tokens. + u"issuer-public-key": next(iter(allowed_keys)), + } + ), allowed_public_keys=just(allowed_keys), ) + return dummy_ristretto_keys_sets().flatmap(share_a_key) @@ -309,42 +298,58 @@ def client_doublespendredeemer_configurations(default_token_counts=token_counts( """ Build DoubleSpendRedeemer-using configuration values for the client-side plugin. """ - return zkapauthz_configuration(just({ - u"redeemer": u"double-spend", - })) + return zkapauthz_configuration( + just( + { + u"redeemer": u"double-spend", + } + ) + ) def client_unpaidredeemer_configurations(): """ Build UnpaidRedeemer-using configuration values for the client-side plugin. """ - return zkapauthz_configuration(just({ - u"redeemer": u"unpaid", - })) + return zkapauthz_configuration( + just( + { + u"redeemer": u"unpaid", + } + ) + ) def client_nonredeemer_configurations(): """ Build NonRedeemer-using configuration values for the client-side plugin. """ - return zkapauthz_configuration(just({ - u"redeemer": u"non", - })) + return zkapauthz_configuration( + just( + { + u"redeemer": u"non", + } + ) + ) def client_errorredeemer_configurations(details): """ Build ErrorRedeemer-using configuration values for the client-side plugin. """ - return zkapauthz_configuration(just({ - u"redeemer": u"error", - u"details": details, - })) + return zkapauthz_configuration( + just( + { + u"redeemer": u"error", + u"details": details, + } + ) + ) def direct_tahoe_configs( - zkapauthz_v1_configuration=client_dummyredeemer_configurations(), - shares=just((None, None, None)), + zkapauthz_v1_configuration=client_dummyredeemer_configurations(), + shares=just((None, None, None)), ): """ Build complete Tahoe-LAFS configurations including the zkapauthorizer @@ -355,9 +360,12 @@ def direct_tahoe_configs( :return SearchStrategy[_Config]: A strategy that builds Tahoe config objects. """ - config_texts = minimal_tahoe_configs({ - u"privatestorageio-zkapauthz-v1": zkapauthz_v1_configuration, - }, shares) + config_texts = minimal_tahoe_configs( + { + u"privatestorageio-zkapauthz-v1": zkapauthz_v1_configuration, + }, + shares, + ) return config_texts.map( lambda config_text: config_from_string( u"/dev/null/illegal", @@ -368,8 +376,8 @@ def direct_tahoe_configs( def tahoe_configs( - zkapauthz_v1_configuration=client_dummyredeemer_configurations(), - shares=just((None, None, None)), + zkapauthz_v1_configuration=client_dummyredeemer_configurations(), + shares=just((None, None, None)), ): """ Build complete Tahoe-LAFS configurations including the zkapauthorizer @@ -384,16 +392,16 @@ def tahoe_configs( are the ``basedir`` and ``portnumfile`` arguments to Tahoe's ``config_from_string.`` """ + def path_setter(config): def set_paths(basedir, portnumfile): config._basedir = basedir.decode("ascii") config.portnum_fname = portnumfile return config + return set_paths - return direct_tahoe_configs( - zkapauthz_v1_configuration, - shares, - ).map( + + return direct_tahoe_configs(zkapauthz_v1_configuration, shares,).map( path_setter, ) @@ -403,11 +411,7 @@ def share_parameters(): Build three-tuples of integers giving usable k, happy, N parameters to Tahoe-LAFS' erasure encoding process. """ - return lists( - integers(min_value=1, max_value=255), - min_size=3, - max_size=3, - ).map( + return lists(integers(min_value=1, max_value=255), min_size=3, max_size=3,).map( sorted, ) @@ -416,13 +420,17 @@ def vouchers(): """ Build unicode strings in the format of vouchers. """ - return binary( - min_size=32, - max_size=32, - ).map( - urlsafe_b64encode, - ).map( - lambda voucher: voucher.decode("ascii"), + return ( + binary( + min_size=32, + max_size=32, + ) + .map( + urlsafe_b64encode, + ) + .map( + lambda voucher: voucher.decode("ascii"), + ) ) @@ -516,13 +524,12 @@ def byte_strings(label, length, entropy): potentially the entire length is random. """ if len(label) + entropy > length: - raise ValueError("Entropy and label don't fit into {} bytes".format( - length, - )) - return binary( - min_size=entropy, - max_size=entropy, - ).map( + raise ValueError( + "Entropy and label don't fit into {} bytes".format( + length, + ) + ) + return binary(min_size=entropy, max_size=entropy,).map( lambda bs: label + b"x" * (length - entropy - len(label)) + bs, ) @@ -531,14 +538,18 @@ def random_tokens(): """ Build ``RandomToken`` instances. """ - return byte_strings( - label=b"random-tokens", - length=_TOKEN_LENGTH, - entropy=4, - ).map( - b64encode, - ).map( - lambda token: RandomToken(token.decode("ascii")), + return ( + byte_strings( + label=b"random-tokens", + length=_TOKEN_LENGTH, + entropy=4, + ) + .map( + b64encode, + ) + .map( + lambda token: RandomToken(token.decode("ascii")), + ) ) @@ -586,14 +597,18 @@ def unblinded_tokens(): base64 encode data. You cannot use these in the PrivacyPass cryptographic protocol but you can put them into the database and take them out again. """ - return byte_strings( - label=b"unblinded-tokens", - length=_UNBLINDED_TOKEN_LENGTH, - entropy=4, - ).map( - b64encode, - ).map( - lambda zkap: UnblindedToken(zkap.decode("ascii")), + return ( + byte_strings( + label=b"unblinded-tokens", + length=_UNBLINDED_TOKEN_LENGTH, + entropy=4, + ) + .map( + b64encode, + ) + .map( + lambda zkap: UnblindedToken(zkap.decode("ascii")), + ) ) @@ -729,10 +744,7 @@ def shares(): """ Build Tahoe-LAFS share data. """ - return tuples( - sharenums(), - sizes() - ).map( + return tuples(sharenums(), sizes()).map( lambda num_and_size: bytes_for_share(*num_and_size), ) @@ -775,6 +787,7 @@ class TestAndWriteVectors(object): ``tw_vectors`` parameter accepted by ``RIStorageServer.slot_testv_and_readv_and_writev``. """ + test_vector = attr.ib() write_vector = attr.ib() new_length = attr.ib() @@ -822,13 +835,16 @@ def announcements(): """ Build announcements for the ZKAPAuthorizer plugin. """ - return just({ - u"ristretto-issuer-root-url": u"https://issuer.example.invalid/", - }) + 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 @@ -869,10 +885,12 @@ def clocks(now=posix_timestamps()): :param now: A strategy that builds POSIX timestamps (ie, ints or floats in the range of time_t). """ + def clock_at_time(when): c = Clock() c.advance(when) return c + return now.map(clock_at_time) @@ -936,6 +954,7 @@ def node_hierarchies(): Build hierarchies of ``IDirectoryNode`` and other ``IFilesystemNode`` (incomplete) providers. """ + def storage_indexes_are_distinct(nodes): seen = set() for n in nodes.flatten(): @@ -945,10 +964,7 @@ def node_hierarchies(): seen.add(si) return True - return recursive( - leaf_nodes(), - directory_nodes, - ).filter( + return recursive(leaf_nodes(), directory_nodes,).filter( storage_indexes_are_distinct, ) @@ -975,22 +991,26 @@ def ristretto_signing_keys(): Build byte strings holding base64-encoded Ristretto signing keys, perhaps with leading or trailing whitespace. """ - keys = sampled_from([ - # A few legit keys - b"mkQf85V2vyLQRUYuqRb+Ke6K+M9pOtXm4MslsuCdBgg=", - b"6f93OIdZHHAmSIaRXDSIU1UcN+sbDAh41TRPb5DhrgI=", - b"k58h8yPT18epw+EKMJhwHFfoM6r3TIExKm4efQHNBgM=", - b"rbaAlWZ3NCnl5oZ9meviGfpLbyJpgpuiuFOX0rLnNwQ=", - ]) - whitespace = sampled_from([ - # maybe no whitespace at all - b"" - # or maybe some - b" ", - b"\t", - b"\n", - b"\r\n", - ]) + keys = sampled_from( + [ + # A few legit keys + b"mkQf85V2vyLQRUYuqRb+Ke6K+M9pOtXm4MslsuCdBgg=", + b"6f93OIdZHHAmSIaRXDSIU1UcN+sbDAh41TRPb5DhrgI=", + b"k58h8yPT18epw+EKMJhwHFfoM6r3TIExKm4efQHNBgM=", + b"rbaAlWZ3NCnl5oZ9meviGfpLbyJpgpuiuFOX0rLnNwQ=", + ] + ) + whitespace = sampled_from( + [ + # maybe no whitespace at all + b"" + # or maybe some + b" ", + b"\t", + b"\n", + b"\r\n", + ] + ) return builds( lambda leading, key, trailing: leading + key + trailing, diff --git a/src/_zkapauthorizer/tests/test_base64.py b/src/_zkapauthorizer/tests/test_base64.py index 4a988b6beae887138a8379210f2e76cae0aaab53..d91bbacd065acc75295c5b0bc8b2d67d790bc2f3 100644 --- a/src/_zkapauthorizer/tests/test_base64.py +++ b/src/_zkapauthorizer/tests/test_base64.py @@ -16,37 +16,23 @@ Tests for ``_zkapauthorizer._base64``. """ -from __future__ import ( - absolute_import, -) +from __future__ import absolute_import -from base64 import ( - urlsafe_b64encode, -) +from base64 import urlsafe_b64encode -from testtools import ( - TestCase, -) -from testtools.matchers import ( - Equals, -) +from hypothesis import given +from hypothesis.strategies import binary +from testtools import TestCase +from testtools.matchers import Equals -from hypothesis import ( - given, -) -from hypothesis.strategies import ( - binary, -) - -from .._base64 import ( - urlsafe_b64decode, -) +from .._base64 import urlsafe_b64decode class Base64Tests(TestCase): """ Tests for ``urlsafe_b64decode``. """ + @given(binary()) def test_roundtrip(self, bytestring): """ diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py index e420b676f2171f804c989b9e183bce831250dd97..954ad66d0dcf99d73bff11e75d443b2d1345dfad 100644 --- a/src/_zkapauthorizer/tests/test_client_resource.py +++ b/src/_zkapauthorizer/tests/test_client_resource.py @@ -17,178 +17,97 @@ Tests for the web resource provided by the client part of the Tahoe-LAFS plugin. """ -from __future__ import ( - absolute_import, -) - -import attr +from __future__ import absolute_import -from .._base64 import ( - urlsafe_b64decode, -) - -from datetime import ( - datetime, -) -from json import ( - dumps, -) -from io import ( - BytesIO, -) -from urllib import ( - quote, -) +from datetime import datetime +from io import BytesIO +from json import dumps +from urllib import quote -from testtools import ( - TestCase, +import attr +from allmydata.client import config_from_string +from aniso8601 import parse_datetime +from fixtures import TempDir +from hypothesis import given, note +from hypothesis.strategies import ( + binary, + builds, + datetimes, + dictionaries, + fixed_dictionaries, + integers, + just, + lists, + none, + one_of, + sampled_from, + text, + tuples, ) +from testtools import TestCase +from testtools.content import text_content from testtools.matchers import ( - MatchesStructure, - MatchesAll, - MatchesAny, - MatchesPredicate, + AfterPreprocessing, AllMatch, - HasLength, - IsInstance, + Always, ContainsDict, - AfterPreprocessing, Equals, - Always, GreaterThan, + HasLength, Is, + IsInstance, + MatchesAll, + MatchesAny, + MatchesPredicate, + MatchesStructure, ) -from testtools.twistedsupport import ( - CaptureTwistedLogs, - succeeded, -) -from testtools.content import ( - text_content, -) - -from aniso8601 import ( - parse_datetime, -) - -from fixtures import ( - TempDir, -) - -from hypothesis import ( - given, - note, -) -from hypothesis.strategies import ( - one_of, - none, - just, - fixed_dictionaries, - sampled_from, - lists, - integers, - binary, - text, - datetimes, - builds, - tuples, - dictionaries, -) - -from twisted.python.filepath import ( - FilePath, -) -from twisted.internet.defer import ( - Deferred, - maybeDeferred, - gatherResults, -) -from twisted.internet.task import ( - Cooperator, - Clock, -) -from twisted.web.http import ( - OK, - UNAUTHORIZED, - NOT_FOUND, - BAD_REQUEST, - NOT_IMPLEMENTED, -) -from twisted.web.http_headers import ( - Headers, -) -from twisted.web.resource import ( - IResource, - getChildForRequest, -) -from twisted.web.client import ( - FileBodyProducer, - readBody, -) - -from treq.testing import ( - RequestTraversalAgent, -) - -from allmydata.client import ( - config_from_string, -) - -from .. import ( - __version__ as zkapauthorizer_version, -) - +from testtools.twistedsupport import CaptureTwistedLogs, succeeded +from treq.testing import RequestTraversalAgent +from twisted.internet.defer import Deferred, gatherResults, maybeDeferred +from twisted.internet.task import Clock, Cooperator +from twisted.python.filepath import FilePath +from twisted.web.client import FileBodyProducer, readBody +from twisted.web.http import BAD_REQUEST, NOT_FOUND, NOT_IMPLEMENTED, OK, UNAUTHORIZED +from twisted.web.http_headers import Headers +from twisted.web.resource import IResource, getChildForRequest + +from .. import __version__ as zkapauthorizer_version +from .._base64 import urlsafe_b64decode +from ..configutil import config_string_from_sections from ..model import ( - Voucher, - Redeeming, - Redeemed, DoubleSpend, - Unpaid, Error, + Redeemed, + Redeeming, + Unpaid, + Voucher, VoucherStore, memory_connect, ) -from ..resource import ( - NUM_TOKENS, - from_configuration, - get_token_count, -) - -from ..pricecalculator import ( - PriceCalculator, -) -from ..configutil import ( - config_string_from_sections, -) - +from ..pricecalculator import PriceCalculator +from ..resource import NUM_TOKENS, from_configuration, get_token_count from ..storage_common import ( - required_passes, - get_configured_pass_value, - get_configured_lease_duration, get_configured_allowed_public_keys, + get_configured_lease_duration, + get_configured_pass_value, + required_passes, ) - +from .json import loads +from .matchers import Provides, between, matches_response from .strategies import ( - direct_tahoe_configs, - tahoe_configs, - client_unpaidredeemer_configurations, + api_auth_tokens, client_doublespendredeemer_configurations, client_dummyredeemer_configurations, - client_nonredeemer_configurations, client_errorredeemer_configurations, - unblinded_tokens, - vouchers, - requests, + client_nonredeemer_configurations, + client_unpaidredeemer_configurations, + direct_tahoe_configs, request_paths, - api_auth_tokens, + requests, share_parameters, -) -from .matchers import ( - Provides, - matches_response, - between, -) -from .json import ( - loads, + tahoe_configs, + unblinded_tokens, + vouchers, ) TRANSIENT_ERROR = u"something went wrong, who knows what" @@ -222,15 +141,14 @@ def not_vouchers(): """ return one_of( text().filter( - lambda t: ( - not is_urlsafe_base64(t) - ), + lambda t: (not is_urlsafe_base64(t)), ), vouchers().map( # Turn a valid voucher into a voucher that is invalid only by # containing a character from the base64 alphabet in place of one # from the urlsafe-base64 alphabet. - lambda voucher: u"/" + voucher[1:], + lambda voucher: u"/" + + voucher[1:], ), ) @@ -254,16 +172,20 @@ def invalid_bodies(): """ return one_of( # The wrong key but the right kind of value. - fixed_dictionaries({ - u"some-key": vouchers(), - }).map(dumps), + fixed_dictionaries( + { + u"some-key": vouchers(), + } + ).map(dumps), # The right key but the wrong kind of value. - fixed_dictionaries({ - u"voucher": one_of( - integers(), - not_vouchers(), - ), - }).map(dumps), + fixed_dictionaries( + { + u"voucher": one_of( + integers(), + not_vouchers(), + ), + } + ).map(dumps), # Not even JSON binary().filter(is_not_json), ) @@ -369,6 +291,7 @@ class FromConfigurationTests(TestCase): """ Tests for ``from_configuration``. """ + @given(tahoe_configs()) def test_allowed_public_keys(self, get_config): """ @@ -393,6 +316,7 @@ class GetTokenCountTests(TestCase): """ Tests for ``get_token_count``. """ + @given(one_of(none(), integers(min_value=16))) def test_get_token_count(self, token_count): """ @@ -405,13 +329,15 @@ class GetTokenCountTests(TestCase): token_config = {} else: expected_count = token_count - token_config = { - u"default-token-count": u"{}".format(expected_count) - } + token_config = {u"default-token-count": u"{}".format(expected_count)} - config_text = config_string_from_sections([{ - u"storageclient.plugins." + plugin_name: token_config, - }]) + config_text = config_string_from_sections( + [ + { + u"storageclient.plugins." + plugin_name: token_config, + } + ] + ) node_config = config_from_string( self.useFixture(TempDir()).join(b"tahoe"), u"tub.port", @@ -427,6 +353,7 @@ class ResourceTests(TestCase): """ General tests for the resources exposed by the plugin. """ + @given( tahoe_configs(), request_paths(), @@ -459,11 +386,15 @@ class ResourceTests(TestCase): @given( tahoe_configs(), - requests(sampled_from([ - [b"unblinded-token"], - [b"voucher"], - [b"version"], - ])), + requests( + sampled_from( + [ + [b"unblinded-token"], + [b"voucher"], + [b"version"], + ] + ) + ), ) def test_reachable(self, get_config, request): """ @@ -534,11 +465,11 @@ class UnblindedTokenTests(TestCase): Tests relating to ``/unblinded-token`` as implemented by the ``_zkapauthorizer.resource`` module. """ + def setUp(self): super(UnblindedTokenTests, self).setUp() self.useFixture(CaptureTwistedLogs()) - @given( tahoe_configs(), api_auth_tokens(), @@ -558,11 +489,15 @@ class UnblindedTokenTests(TestCase): ) root = root_from_config(config, datetime.now) agent = RequestTraversalAgent(root) - data = BytesIO(dumps({u"unblinded-tokens": list( - token.unblinded_token - for token - in unblinded_tokens - )})) + data = BytesIO( + dumps( + { + u"unblinded-tokens": list( + token.unblinded_token for token in unblinded_tokens + ) + } + ) + ) requesting = authorized_request( api_auth_token, @@ -582,11 +517,7 @@ class UnblindedTokenTests(TestCase): self.assertThat( stored_tokens, - Equals(list( - token.unblinded_token - for token - in unblinded_tokens - )), + Equals(list(token.unblinded_token for token in unblinded_tokens)), ) @given( @@ -694,7 +625,9 @@ class UnblindedTokenTests(TestCase): maybe_extra_tokens(), text(max_size=64), ) - def test_get_position(self, get_config, api_auth_token, voucher, extra_tokens, position): + def test_get_position( + self, get_config, api_auth_token, voucher, extra_tokens, position + ): """ When the unblinded token collection receives a **GET** with a **position** query argument, it returns all unblinded tokens which sort greater @@ -754,16 +687,21 @@ class UnblindedTokenTests(TestCase): integers(min_value=1, max_value=16), integers(min_value=1, max_value=128), ) - def test_get_order_matches_use_order(self, get_config, api_auth_token, voucher, num_redemption_groups, extra_tokens): + def test_get_order_matches_use_order( + self, get_config, api_auth_token, voucher, num_redemption_groups, extra_tokens + ): """ The first unblinded token returned in a response to a **GET** request is the first token to be used to authorize a storage request. """ + def after(d, f): new_d = Deferred() + def f_and_continue(result): maybeDeferred(f).chainDeferred(new_d) return result + d.addCallback(f_and_continue) return new_d @@ -812,7 +750,8 @@ class UnblindedTokenTests(TestCase): gatherResults([getting_initial_tokens, getting_tokens_after]), succeeded( MatchesPredicate( - lambda (initial_tokens, tokens_after): initial_tokens[1:] == tokens_after, + lambda (initial_tokens, tokens_after): initial_tokens[1:] + == tokens_after, u"initial, after (%s): initial[1:] != after", ), ), @@ -829,7 +768,9 @@ class UnblindedTokenTests(TestCase): ), datetimes(), ) - def test_latest_lease_maintenance_spending(self, get_config, api_auth_token, size_observations, now): + def test_latest_lease_maintenance_spending( + self, get_config, api_auth_token, size_observations, now + ): """ The most recently completed record of lease maintenance spending activity is reported in the response to a **GET** request. @@ -862,18 +803,22 @@ class UnblindedTokenTests(TestCase): ) self.assertThat( d, - succeeded(Equals({ - "when": now.isoformat(), - "count": total, - })), + succeeded( + Equals( + { + "when": now.isoformat(), + "count": total, + } + ) + ), ) def succeeded_with_unblinded_tokens_with_matcher( - all_token_count, - match_spendable_token_count, - match_unblinded_tokens, - match_lease_maint_spending, + all_token_count, + match_spendable_token_count, + match_unblinded_tokens, + match_lease_maint_spending, ): """ :return: A matcher which matches a Deferred which fires with a response @@ -894,17 +839,20 @@ def succeeded_with_unblinded_tokens_with_matcher( AfterPreprocessing( json_content, succeeded( - ContainsDict({ - u"total": Equals(all_token_count), - u"spendable": match_spendable_token_count, - u"unblinded-tokens": match_unblinded_tokens, - u"lease-maintenance-spending": match_lease_maint_spending, - }), + ContainsDict( + { + u"total": Equals(all_token_count), + u"spendable": match_spendable_token_count, + u"unblinded-tokens": match_unblinded_tokens, + u"lease-maintenance-spending": match_lease_maint_spending, + } + ), ), ), ), ) + def succeeded_with_unblinded_tokens(all_token_count, returned_token_count): """ :return: A matcher which matches a Deferred which fires with a response @@ -926,6 +874,7 @@ def succeeded_with_unblinded_tokens(all_token_count, returned_token_count): match_lease_maint_spending=matches_lease_maintenance_spending(), ) + def matches_lease_maintenance_spending(): """ :return: A matcher which matches the value of the @@ -934,18 +883,22 @@ def matches_lease_maintenance_spending(): """ return MatchesAny( Is(None), - ContainsDict({ - u"when": matches_iso8601_datetime(), - u"amount": matches_positive_integer(), - }), + ContainsDict( + { + u"when": matches_iso8601_datetime(), + u"amount": matches_positive_integer(), + } + ), ) + def matches_positive_integer(): return MatchesAll( IsInstance(int), GreaterThan(0), ) + def matches_iso8601_datetime(): """ :return: A matcher which matches unicode strings which can be parsed as an @@ -959,17 +912,18 @@ def matches_iso8601_datetime(): ), ) + class VoucherTests(TestCase): """ Tests relating to ``/voucher`` as implemented by the ``_zkapauthorizer.resource`` module and its handling of vouchers. """ + def setUp(self): super(VoucherTests, self).setUp() self.useFixture(CaptureTwistedLogs()) - @given(tahoe_configs(), api_auth_tokens(), vouchers()) def test_put_voucher(self, get_config, api_auth_token, voucher): """ @@ -1067,7 +1021,6 @@ class VoucherTests(TestCase): ), ) - @given(tahoe_configs(), api_auth_tokens(), vouchers()) def test_get_unknown_voucher(self, get_config, api_auth_token, voucher): """ @@ -1118,10 +1071,12 @@ class VoucherTests(TestCase): number=Equals(voucher), expected_tokens=Equals(count), created=Equals(now), - state=Equals(Redeeming( - started=now, - counter=0, - )), + state=Equals( + Redeeming( + started=now, + counter=0, + ) + ), ), ) @@ -1148,10 +1103,12 @@ class VoucherTests(TestCase): number=Equals(voucher), expected_tokens=Equals(count), created=Equals(now), - state=Equals(Redeemed( - finished=now, - token_count=count, - )), + state=Equals( + Redeemed( + finished=now, + token_count=count, + ) + ), ), ) @@ -1179,9 +1136,11 @@ class VoucherTests(TestCase): number=Equals(voucher), expected_tokens=Equals(count), created=Equals(now), - state=Equals(DoubleSpend( - finished=now, - )), + state=Equals( + DoubleSpend( + finished=now, + ) + ), ), ) @@ -1209,9 +1168,11 @@ class VoucherTests(TestCase): number=Equals(voucher), expected_tokens=Equals(count), created=Equals(now), - state=Equals(Unpaid( - finished=now, - )), + state=Equals( + Unpaid( + finished=now, + ) + ), ), ) @@ -1239,14 +1200,18 @@ class VoucherTests(TestCase): number=Equals(voucher), expected_tokens=Equals(count), created=Equals(now), - state=Equals(Error( - finished=now, - details=TRANSIENT_ERROR, - )), + state=Equals( + Error( + finished=now, + details=TRANSIENT_ERROR, + ) + ), ), ) - def _test_get_known_voucher(self, config, api_auth_token, now, voucher, voucher_matcher): + def _test_get_known_voucher( + self, config, api_auth_token, now, voucher, voucher_matcher + ): """ Assert that a voucher that is ``PUT`` and then ``GET`` is represented in the JSON response. @@ -1321,21 +1286,22 @@ class VoucherTests(TestCase): api_auth_token, now, vouchers, - Equals({ - u"vouchers": list( - Voucher( - number=voucher, - expected_tokens=count, - created=now, - state=Redeemed( - finished=now, - token_count=count, - ), - ).marshal() - for voucher - in vouchers - ), - }), + Equals( + { + u"vouchers": list( + Voucher( + number=voucher, + expected_tokens=count, + created=now, + state=Redeemed( + finished=now, + token_count=count, + ), + ).marshal() + for voucher in vouchers + ), + } + ), ) @given( @@ -1344,7 +1310,9 @@ class VoucherTests(TestCase): datetimes(), lists(vouchers(), unique=True), ) - def test_list_vouchers_transient_states(self, config, api_auth_token, now, vouchers): + def test_list_vouchers_transient_states( + self, config, api_auth_token, now, vouchers + ): """ A ``GET`` to the ``VoucherCollection`` itself returns a list of existing vouchers including state information that reflects transient states. @@ -1355,23 +1323,26 @@ class VoucherTests(TestCase): api_auth_token, now, vouchers, - Equals({ - u"vouchers": list( - Voucher( - number=voucher, - expected_tokens=count, - created=now, - state=Unpaid( - finished=now, - ), - ).marshal() - for voucher - in vouchers - ), - }), + Equals( + { + u"vouchers": list( + Voucher( + number=voucher, + expected_tokens=count, + created=now, + state=Unpaid( + finished=now, + ), + ).marshal() + for voucher in vouchers + ), + } + ), ) - def _test_list_vouchers(self, config, api_auth_token, now, vouchers, match_response_object): + def _test_list_vouchers( + self, config, api_auth_token, now, vouchers, match_response_object + ): add_api_token_to_config( # Hypothesis causes our test case instances to be re-used many # times between setUp and tearDown. Avoid re-using the same @@ -1434,13 +1405,17 @@ def mime_types(blacklist=None): """ if blacklist is None: blacklist = set() - return tuples( - text(), - text(), - ).map( - b"/".join, - ).filter( - lambda content_type: content_type not in blacklist, + return ( + tuples( + text(), + text(), + ) + .map( + b"/".join, + ) + .filter( + lambda content_type: content_type not in blacklist, + ) ) @@ -1449,6 +1424,7 @@ class Request(object): """ Represent some of the parameters of an HTTP request. """ + method = attr.ib() headers = attr.ib() data = attr.ib() @@ -1460,23 +1436,25 @@ def bad_calculate_price_requests(): ``/calculate-price`` endpoint. """ good_methods = just(b"POST") - bad_methods = sampled_from([ - b"GET", - b"HEAD", - b"PUT", - b"PATCH", - b"OPTIONS", - b"FOO", - ]) + bad_methods = sampled_from( + [ + b"GET", + b"HEAD", + b"PUT", + b"PATCH", + b"OPTIONS", + b"FOO", + ] + ) good_headers = just({b"content-type": [b"application/json"]}) - bad_headers = fixed_dictionaries({ - b"content-type": mime_types( - blacklist={b"application/json"}, - ).map( - lambda content_type: [content_type], - ), - }) + bad_headers = fixed_dictionaries( + { + b"content-type": mime_types(blacklist={b"application/json"},).map( + lambda content_type: [content_type], + ), + } + ) good_version = just(1) bad_version = one_of( @@ -1495,20 +1473,26 @@ def bad_calculate_price_requests(): lists(integers(max_value=-1), min_size=1), ) - good_data = fixed_dictionaries({ - u"version": good_version, - u"sizes": good_sizes, - }).map(dumps) + good_data = fixed_dictionaries( + { + u"version": good_version, + u"sizes": good_sizes, + } + ).map(dumps) - bad_data_version = fixed_dictionaries({ - u"version": bad_version, - u"sizes": good_sizes, - }).map(dumps) + bad_data_version = fixed_dictionaries( + { + u"version": bad_version, + u"sizes": good_sizes, + } + ).map(dumps) - bad_data_sizes = fixed_dictionaries({ - u"version": good_version, - u"sizes": bad_sizes, - }).map(dumps) + bad_data_sizes = fixed_dictionaries( + { + u"version": good_version, + u"sizes": bad_sizes, + } + ).map(dumps) bad_data_other = dictionaries( text(), @@ -1537,13 +1521,8 @@ def bad_calculate_price_requests(): fields[key] = value return fields - return sampled_from( - bad_choices, - ).flatmap( - lambda bad_choice: builds( - Request, - **merge(good_fields, *bad_choice) - ), + return sampled_from(bad_choices,).flatmap( + lambda bad_choice: builds(Request, **merge(good_fields, *bad_choice)), ) @@ -1552,6 +1531,7 @@ class CalculatePriceTests(TestCase): Tests relating to ``/calculate-price`` as implemented by the ``_zkapauthorizer.resource`` module. """ + url = b"http://127.0.0.1/calculate-price" @given( @@ -1617,7 +1597,9 @@ class CalculatePriceTests(TestCase): api_auth_tokens(), lists(integers(min_value=0)), ) - def test_calculated_price(self, encoding_params_and_get_config, api_auth_token, sizes): + def test_calculated_price( + self, encoding_params_and_get_config, api_auth_token, sizes + ): """ A well-formed request returns the price in ZKAPs as an integer and the storage period (the minimum allowed) that they pay for. @@ -1654,10 +1636,12 @@ class CalculatePriceTests(TestCase): headers_matcher=application_json(), body_matcher=AfterPreprocessing( loads, - Equals({ - u"price": expected_price, - u"period": get_configured_lease_duration(config), - }), + Equals( + { + u"price": expected_price, + u"period": get_configured_lease_duration(config), + } + ), ), ), ), @@ -1705,10 +1689,12 @@ class _MatchResponse(object): _details = attr.ib(default=attr.Factory(dict)) def match(self, response): - self._details.update({ - u"code": response.code, - u"headers": response.headers.getAllRawHeaders(), - }) + self._details.update( + { + u"code": response.code, + u"headers": response.headers.getAllRawHeaders(), + } + ) return MatchesStructure( code=self.code, headers=self.headers, diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py index cf7726f913566ca06412d523b9c80c7126987017..c063eb093f5516fe899916aac1fa38df2381d927 100644 --- a/src/_zkapauthorizer/tests/test_controller.py +++ b/src/_zkapauthorizer/tests/test_controller.py @@ -16,138 +16,77 @@ Tests for ``_zkapauthorizer.controller``. """ -from __future__ import ( - absolute_import, - division, -) - -from json import ( - loads, - dumps, -) -from functools import ( - partial, -) -from datetime import ( - datetime, - timedelta, -) -from zope.interface import ( - implementer, -) -from testtools import ( - TestCase, -) -from testtools.content import ( - text_content, -) -from testtools.matchers import ( - Always, - Equals, - MatchesAll, - AllMatch, - IsInstance, - HasLength, - AfterPreprocessing, - MatchesStructure, -) -from testtools.twistedsupport import ( - succeeded, - has_no_result, - failed, -) +from __future__ import absolute_import, division -from hypothesis import ( - given, - assume, -) -from hypothesis.strategies import ( - integers, - datetimes, - lists, - sampled_from, - randoms, -) -from twisted.python.url import ( - URL, -) -from twisted.internet.defer import ( - fail, -) -from twisted.internet.task import ( - Clock, -) -from twisted.web.iweb import ( - IAgent, -) -from twisted.web.resource import ( - ErrorPage, - Resource, -) -from twisted.web.http_headers import ( - Headers, -) -from twisted.web.http import ( - UNSUPPORTED_MEDIA_TYPE, - BAD_REQUEST, - INTERNAL_SERVER_ERROR, -) -from treq.testing import ( - StubTreq, -) +from datetime import datetime, timedelta +from functools import partial +from json import dumps, loads from challenge_bypass_ristretto import ( - SecurityException, - PublicKey, - BlindedToken, BatchDLEQProof, + BlindedToken, + PublicKey, + SecurityException, TokenPreimage, VerificationSignature, random_signing_key, ) +from hypothesis import assume, given +from hypothesis.strategies import datetimes, integers, lists, randoms, sampled_from +from testtools import TestCase +from testtools.content import text_content +from testtools.matchers import ( + AfterPreprocessing, + AllMatch, + Always, + Equals, + HasLength, + IsInstance, + MatchesAll, + MatchesStructure, +) +from testtools.twistedsupport import failed, has_no_result, succeeded +from treq.testing import StubTreq +from twisted.internet.defer import fail +from twisted.internet.task import Clock +from twisted.python.url import URL +from twisted.web.http import BAD_REQUEST, INTERNAL_SERVER_ERROR, UNSUPPORTED_MEDIA_TYPE +from twisted.web.http_headers import Headers +from twisted.web.iweb import IAgent +from twisted.web.resource import ErrorPage, Resource +from zope.interface import implementer from ..controller import ( - IRedeemer, - NonRedeemer, - DummyRedeemer, + AlreadySpent, DoubleSpendRedeemer, - UnpaidRedeemer, - RistrettoRedeemer, + DummyRedeemer, IndexedRedeemer, - RecordingRedeemer, + IRedeemer, + NonRedeemer, PaymentController, + RecordingRedeemer, + RistrettoRedeemer, UnexpectedResponse, - AlreadySpent, Unpaid, + UnpaidRedeemer, token_count_for_group, ) - -from ..model import ( - UnblindedToken, - Pending as model_Pending, - Redeeming as model_Redeeming, - DoubleSpend as model_DoubleSpend, - Redeemed as model_Redeemed, - Unpaid as model_Unpaid, -) - +from ..model import DoubleSpend as model_DoubleSpend +from ..model import Pending as model_Pending +from ..model import Redeemed as model_Redeemed +from ..model import Redeeming as model_Redeeming +from ..model import UnblindedToken +from ..model import Unpaid as model_Unpaid +from .fixtures import ConfiglessMemoryVoucherStore, TemporaryVoucherStore +from .matchers import Provides, between, raises from .strategies import ( + clocks, + dummy_ristretto_keys, + redemption_group_counts, tahoe_configs, - vouchers, - voucher_objects, voucher_counters, - redemption_group_counts, - dummy_ristretto_keys, - clocks, -) -from .matchers import ( - Provides, - raises, - between, -) -from .fixtures import ( - TemporaryVoucherStore, - ConfiglessMemoryVoucherStore, + voucher_objects, + vouchers, ) @@ -155,6 +94,7 @@ class TokenCountForGroupTests(TestCase): """ Tests for ``token_count_for_group``. """ + @given( integers(), integers(), @@ -167,9 +107,7 @@ class TokenCountForGroupTests(TestCase): range then ``ValueError`` is raised. """ assume( - group_number < 0 or - group_number >= num_groups or - total_tokens < num_groups + group_number < 0 or group_number >= num_groups or total_tokens < num_groups ) self.assertThat( lambda: token_count_for_group(num_groups, total_tokens, group_number), @@ -189,8 +127,7 @@ class TokenCountForGroupTests(TestCase): self.assertThat( sum( token_count_for_group(num_groups, total_tokens, group_number) - for group_number - in range(num_groups) + for group_number in range(num_groups) ), Equals(total_tokens), ) @@ -211,8 +148,7 @@ class TokenCountForGroupTests(TestCase): self.assertThat( list( token_count_for_group(num_groups, total_tokens, group_number) - for group_number - in range(num_groups) + for group_number in range(num_groups) ), AllMatch(between(lower_bound, upper_bound)), ) @@ -222,6 +158,7 @@ class PaymentControllerTests(TestCase): """ Tests for ``PaymentController``. """ + @given(tahoe_configs(), datetimes(), vouchers(), dummy_ristretto_keys()) def test_should_not_redeem(self, get_config, now, voucher, public_key): """ @@ -284,7 +221,13 @@ class PaymentControllerTests(TestCase): Equals(model_Pending(counter=0)), ) - @given(tahoe_configs(), datetimes(), vouchers(), voucher_counters(), dummy_ristretto_keys()) + @given( + tahoe_configs(), + datetimes(), + vouchers(), + voucher_counters(), + dummy_ristretto_keys(), + ) def test_redeeming(self, get_config, now, voucher, num_successes, public_key): """ A ``Voucher`` is marked redeeming while ``IRedeemer.redeem`` is actively @@ -296,8 +239,7 @@ class PaymentControllerTests(TestCase): # that. counter = num_successes + 1 redeemer = IndexedRedeemer( - [DummyRedeemer(public_key)] * num_successes + - [NonRedeemer()], + [DummyRedeemer(public_key)] * num_successes + [NonRedeemer()], ) store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store controller = PaymentController( @@ -320,10 +262,12 @@ class PaymentControllerTests(TestCase): controller_voucher = controller.get_voucher(voucher) self.assertThat( controller_voucher.state, - Equals(model_Redeeming( - started=now, - counter=num_successes, - )), + Equals( + model_Redeeming( + started=now, + counter=num_successes, + ) + ), ) @given( @@ -334,7 +278,9 @@ class PaymentControllerTests(TestCase): voucher_counters().map(lambda v: v + 1), dummy_ristretto_keys(), ) - def test_restart_redeeming(self, get_config, now, voucher, before_restart, after_restart, public_key): + def test_restart_redeeming( + self, get_config, now, voucher, before_restart, after_restart, public_key + ): """ If some redemption groups for a voucher have succeeded but the process is interrupted, redemption begins at the first incomplete redemption @@ -360,8 +306,8 @@ class PaymentControllerTests(TestCase): store, # It will let `before_restart` attempts succeed before hanging. IndexedRedeemer( - [DummyRedeemer(public_key)] * before_restart + - [NonRedeemer()] * after_restart, + [DummyRedeemer(public_key)] * before_restart + + [NonRedeemer()] * after_restart, ), default_token_count=num_tokens, num_redemption_groups=num_redemption_groups, @@ -381,8 +327,8 @@ class PaymentControllerTests(TestCase): # It will succeed only for the higher counter values which did # not succeed or did not get started on the first try. IndexedRedeemer( - [NonRedeemer()] * before_restart + - [DummyRedeemer(public_key)] * after_restart, + [NonRedeemer()] * before_restart + + [DummyRedeemer(public_key)] * after_restart, ), # The default token count for this new controller doesn't # matter. The redemption attempt already started with some @@ -410,8 +356,16 @@ class PaymentControllerTests(TestCase): ), ) - @given(tahoe_configs(), datetimes(), vouchers(), voucher_counters(), integers(min_value=0, max_value=100)) - def test_stop_redeeming_on_error(self, get_config, now, voucher, counter, extra_tokens): + @given( + tahoe_configs(), + datetimes(), + vouchers(), + voucher_counters(), + integers(min_value=0, max_value=100), + ) + def test_stop_redeeming_on_error( + self, get_config, now, voucher, counter, extra_tokens + ): """ If an error is encountered on one of the redemption attempts performed by ``IRedeemer.redeem``, the effort is suspended until the normal retry @@ -463,10 +417,12 @@ class PaymentControllerTests(TestCase): persisted_voucher = store.get(voucher) self.assertThat( persisted_voucher.state, - Equals(model_Redeemed( - finished=now, - token_count=100, - )), + Equals( + model_Redeemed( + finished=now, + token_count=100, + ) + ), ) @given(tahoe_configs(), datetimes(), vouchers()) @@ -492,9 +448,11 @@ class PaymentControllerTests(TestCase): self.assertThat( persisted_voucher, MatchesStructure( - state=Equals(model_DoubleSpend( - finished=now, - )), + state=Equals( + model_DoubleSpend( + finished=now, + ) + ), ), ) @@ -578,7 +536,7 @@ class PaymentControllerTests(TestCase): MatchesStructure( finished=Equals(datetime_now()), ), - ) + ), ) # Some time passes. @@ -618,7 +576,10 @@ class PaymentControllerTests(TestCase): unique=True, ).map( # Split the keys into allowed and disallowed groups - lambda public_keys: (public_keys[:num_allowed_key_groups], public_keys[num_allowed_key_groups:]), + lambda public_keys: ( + public_keys[:num_allowed_key_groups], + public_keys[num_allowed_key_groups:], + ), ), ), ), @@ -626,7 +587,9 @@ class PaymentControllerTests(TestCase): # required by the number of redemption groups we have. integers(min_value=0, max_value=32), ) - def test_sequester_tokens_for_untrusted_key(self, random, clock, voucher, public_keys, extra_token_count): + def test_sequester_tokens_for_untrusted_key( + self, random, clock, voucher, public_keys, extra_token_count + ): """ All unblinded tokens which are returned from the redemption process associated with a public key that the controller has not been @@ -655,11 +618,7 @@ class PaymentControllerTests(TestCase): datetime_now = lambda: datetime.utcfromtimestamp(clock.seconds()) store = self.useFixture(ConfiglessMemoryVoucherStore(datetime_now)).store - redeemers = list( - DummyRedeemer(public_key) - for public_key - in all_public_keys - ) + redeemers = list(DummyRedeemer(public_key) for public_key in all_public_keys) controller = PaymentController( store, @@ -678,12 +637,14 @@ class PaymentControllerTests(TestCase): ) def count_in_group(public_keys, key_group): - return sum(( - token_count_for_group(num_redemption_groups, token_count, n) - for n, public_key - in enumerate(public_keys) - if public_key in key_group - ), 0) + return sum( + ( + token_count_for_group(num_redemption_groups, token_count, n) + for n, public_key in enumerate(public_keys) + if public_key in key_group + ), + 0, + ) allowed_token_count = count_in_group(all_public_keys, allowed_public_keys) disallowed_token_count = count_in_group(all_public_keys, disallowed_public_keys) @@ -723,8 +684,7 @@ class PaymentControllerTests(TestCase): unblinded_token for counter, redeemer in enumerate(redeemers) if redeemer._public_key in allowed_public_keys - for unblinded_token - in redeemer.redeemWithCounter( + for unblinded_token in redeemer.redeemWithCounter( voucher_obj, counter, redeemer.random_tokens_for_voucher( @@ -746,10 +706,12 @@ class PaymentControllerTests(TestCase): NOWHERE = URL.from_text(u"https://127.0.0.1/") + class RistrettoRedeemerTests(TestCase): """ Tests for ``RistrettoRedeemer``. """ + def test_interface(self): """ An ``RistrettoRedeemer`` instance provides ``IRedeemer``. @@ -937,9 +899,11 @@ class RistrettoRedeemerTests(TestCase): counter, random_tokens, ) + def unblinded_tokens_to_passes(result): passes = redeemer.tokens_to_passes(message, result.unblinded_tokens) return passes + d.addCallback(unblinded_tokens_to_passes) self.assertThat( @@ -974,39 +938,32 @@ def ristretto_verify(signing_key, message, marshaled_passes): ``marshaled_passes`` pass the Ristretto-defined verification for an exchange using the given signing key and message. """ + def decode(marshaled_pass): t, s = marshaled_pass.split(u" ") return ( TokenPreimage.decode_base64(t.encode("ascii")), VerificationSignature.decode_base64(s.encode("ascii")), ) + servers_passes = list( - decode(marshaled_pass.pass_text) - for marshaled_pass - in marshaled_passes + decode(marshaled_pass.pass_text) for marshaled_pass in marshaled_passes ) servers_unblinded_tokens = list( signing_key.rederive_unblinded_token(token_preimage) - for (token_preimage, sig) - in servers_passes - ) - servers_verification_sigs = list( - sig - for (token_preimage, sig) - in servers_passes + for (token_preimage, sig) in servers_passes ) + servers_verification_sigs = list(sig for (token_preimage, sig) in servers_passes) servers_verification_keys = list( unblinded_token.derive_verification_key_sha512() - for unblinded_token - in servers_unblinded_tokens + for unblinded_token in servers_unblinded_tokens ) invalid_passes = list( key.invalid_sha512( sig, message, ) - for (key, sig) - in zip(servers_verification_keys, servers_verification_sigs) + for (key, sig) in zip(servers_verification_keys, servers_verification_sigs) ) return not any(invalid_passes) @@ -1038,6 +995,7 @@ class UnexpectedResponseRedemption(Resource): An ``UnexpectedResponseRedemption`` simulates the Ristretto redemption server but always returns a non-JSON error response. """ + def render_POST(self, request): request.setResponseCode(INTERNAL_SERVER_ERROR) return b"Sorry, this server does not behave well." @@ -1049,6 +1007,7 @@ class AlreadySpentRedemption(Resource): but always refuses to allow vouchers to be redeemed and reports an error that the voucher has already been redeemed. """ + def render_POST(self, request): request_error = check_redemption_request(request) if request_error is not None: @@ -1063,6 +1022,7 @@ class UnpaidRedemption(Resource): always refuses to allow vouchers to be redeemed and reports an error that the voucher has not been paid for. """ + def render_POST(self, request): request_error = check_redemption_request(request) if request_error is not None: @@ -1086,18 +1046,14 @@ class RistrettoRedemption(Resource): marshaled_blinded_tokens = request_body[u"redeemTokens"] servers_blinded_tokens = list( BlindedToken.decode_base64(marshaled_blinded_token.encode("ascii")) - for marshaled_blinded_token - in marshaled_blinded_tokens + for marshaled_blinded_token in marshaled_blinded_tokens ) servers_signed_tokens = list( self.signing_key.sign(blinded_token) - for blinded_token - in servers_blinded_tokens + for blinded_token in servers_blinded_tokens ) marshaled_signed_tokens = list( - signed_token.encode_base64() - for signed_token - in servers_signed_tokens + signed_token.encode_base64() for signed_token in servers_signed_tokens ) servers_proof = BatchDLEQProof.create( self.signing_key, @@ -1109,18 +1065,21 @@ class RistrettoRedemption(Resource): finally: servers_proof.destroy() - return dumps({ - u"success": True, - u"public-key": self.public_key.encode_base64(), - u"signatures": marshaled_signed_tokens, - u"proof": marshaled_proof, - }) + return dumps( + { + u"success": True, + u"public-key": self.public_key.encode_base64(), + u"signatures": marshaled_signed_tokens, + u"proof": marshaled_proof, + } + ) class CheckRedemptionRequestTests(TestCase): """ Tests for ``check_redemption_request``. """ + def test_content_type(self): """ If the request content-type is not application/json, the response is @@ -1197,6 +1156,7 @@ class CheckRedemptionRequestTests(TestCase): ), ) + def check_redemption_request(request): """ Verify that the given request conforms to the redemption server's public @@ -1218,7 +1178,8 @@ def check_redemption_request(request): actual_keys = set(request_body.keys()) if expected_keys != actual_keys: return bad_request( - request, { + request, + { u"success": False, u"reason": u"{} != {}".format( expected_keys, diff --git a/src/_zkapauthorizer/tests/test_foolscap.py b/src/_zkapauthorizer/tests/test_foolscap.py index 5912b3531c32e8b896834a77f88d66469aea0d85..3a313b879aa720caf31b20ce97b191d9289e1424 100644 --- a/src/_zkapauthorizer/tests/test_foolscap.py +++ b/src/_zkapauthorizer/tests/test_foolscap.py @@ -16,70 +16,30 @@ Tests for Foolscap-related test helpers. """ -from __future__ import ( - absolute_import, -) - -from fixtures import ( - Fixture, -) -from testtools import ( - TestCase, -) +from __future__ import absolute_import + +from fixtures import Fixture +from foolscap.api import Any, RemoteInterface, Violation +from foolscap.furl import decode_furl +from foolscap.pb import Tub +from foolscap.referenceable import RemoteReferenceOnly, RemoteReferenceTracker +from hypothesis import given +from hypothesis.strategies import just, one_of +from testtools import TestCase from testtools.matchers import ( - Equals, - MatchesAll, AfterPreprocessing, Always, + Equals, IsInstance, + MatchesAll, ) -from testtools.twistedsupport import ( - succeeded, - failed, -) - -from twisted.trial.unittest import ( - TestCase as TrialTestCase, -) -from twisted.internet.defer import ( - inlineCallbacks, -) - -from foolscap.api import ( - Violation, - RemoteInterface, - Any, -) -from foolscap.furl import ( - decode_furl, -) -from foolscap.pb import ( - Tub, -) -from foolscap.referenceable import ( - RemoteReferenceTracker, - RemoteReferenceOnly, -) - -from hypothesis import ( - given, -) -from hypothesis.strategies import ( - one_of, - just, -) +from testtools.twistedsupport import failed, succeeded +from twisted.internet.defer import inlineCallbacks +from twisted.trial.unittest import TestCase as TrialTestCase -from .foolscap import ( - RIStub, - Echoer, - LocalRemote, - BrokenCopyable, - DummyReferenceable, -) +from ..foolscap import ShareStat +from .foolscap import BrokenCopyable, DummyReferenceable, Echoer, LocalRemote, RIStub -from ..foolscap import ( - ShareStat, -) class IHasSchema(RemoteInterface): def method(arg=int): @@ -111,6 +71,7 @@ class LocalRemoteTests(TestCase): """ Tests for the ``LocalRemote`` test double. """ + @given( ref=one_of( just(remote_reference()), @@ -195,6 +156,7 @@ class LocalRemoteTests(TestCase): ``LocalRemote.callRemote`` returns a ``Deferred`` that fires with a failure if the method's result cannot be serialized. """ + class BrokenResultReferenceable(DummyReferenceable): def doRemoteCall(self, *a, **kw): return BrokenCopyable() @@ -224,6 +186,7 @@ class SerializationTests(TrialTestCase): """ Tests for the serialization of types used in the Foolscap API. """ + def test_sharestat(self): """ A ``ShareStat`` instance can be sent as an argument to and received in a diff --git a/src/_zkapauthorizer/tests/test_lease_maintenance.py b/src/_zkapauthorizer/tests/test_lease_maintenance.py index 142eff8243620f67de7d2aabf396c3f7badde3b6..2a8743dda3fbf6298f316928f093dd3ce3df5b64 100644 --- a/src/_zkapauthorizer/tests/test_lease_maintenance.py +++ b/src/_zkapauthorizer/tests/test_lease_maintenance.py @@ -16,105 +16,55 @@ Tests for ``_zkapauthorizer.lease_maintenance``. """ -from __future__ import ( - absolute_import, - unicode_literals, -) +from __future__ import absolute_import, unicode_literals -from datetime import ( - datetime, - timedelta, -) +from datetime import datetime, timedelta import attr - -from zope.interface import ( - implementer, -) - -from testtools import ( - TestCase, -) -from testtools.matchers import ( - Is, - Equals, - Always, - HasLength, - MatchesAll, - AllMatch, - AfterPreprocessing, -) -from testtools.twistedsupport import ( - succeeded, -) -from fixtures import ( - TempDir, -) -from hypothesis import ( - given, - note, -) +from allmydata.client import SecretHolder +from allmydata.interfaces import IServer, IStorageBroker +from allmydata.util.hashutil import CRYPTO_VAL_SIZE +from fixtures import TempDir +from hypothesis import given, note from hypothesis.strategies import ( - builds, binary, + builds, + composite, + dictionaries, + floats, integers, + just, lists, - floats, - dictionaries, randoms, - composite, - just, -) - -from twisted.python.filepath import ( - FilePath, -) -from twisted.internet.task import ( - Clock, -) -from twisted.internet.defer import ( - succeed, - maybeDeferred, -) -from twisted.application.service import ( - IService, -) - -from allmydata.util.hashutil import ( - CRYPTO_VAL_SIZE, -) -from allmydata.client import ( - SecretHolder, ) -from allmydata.interfaces import ( - IStorageBroker, - IServer, -) - -from ..foolscap import ( - ShareStat, -) - -from .matchers import ( - Provides, - between, - leases_current, -) -from .strategies import ( - storage_indexes, - clocks, - leaf_nodes, - node_hierarchies, +from testtools import TestCase +from testtools.matchers import ( + AfterPreprocessing, + AllMatch, + Always, + Equals, + HasLength, + Is, + MatchesAll, ) - +from testtools.twistedsupport import succeeded +from twisted.application.service import IService +from twisted.internet.defer import maybeDeferred, succeed +from twisted.internet.task import Clock +from twisted.python.filepath import FilePath +from zope.interface import implementer + +from ..foolscap import ShareStat from ..lease_maintenance import ( - NoopMaintenanceObserver, MemoryMaintenanceObserver, + NoopMaintenanceObserver, lease_maintenance_service, maintain_leases_from_root, - visit_storage_indexes_from_root, renew_leases, + visit_storage_indexes_from_root, ) +from .matchers import Provides, between, leases_current +from .strategies import clocks, leaf_nodes, node_hierarchies, storage_indexes def interval_means(): @@ -146,16 +96,18 @@ class DummyStorageServer(object): :ivar dict[bytes, ShareStat] buckets: A mapping from storage index to metadata about shares at that storage index. """ + clock = attr.ib() buckets = attr.ib() lease_seed = attr.ib() def stat_shares(self, storage_indexes): - return succeed(list( - {0: self.buckets[idx]} if idx in self.buckets else {} - for idx - in storage_indexes - )) + return succeed( + list( + {0: self.buckets[idx]} if idx in self.buckets else {} + for idx in storage_indexes + ) + ) def get_lease_seed(self): return self.lease_seed @@ -227,6 +179,7 @@ class DummyServer(object): """ A partial implementation of a Tahoe-LAFS "native" storage server. """ + _storage_server = attr.ib() def get_storage_server(self): @@ -239,6 +192,7 @@ class DummyStorageBroker(object): """ A partial implementation of a Tahoe-LAFS storage broker. """ + clock = attr.ib() _storage_servers = attr.ib() @@ -259,6 +213,7 @@ class LeaseMaintenanceServiceTests(TestCase): """ Tests for the service returned by ``lease_maintenance_service``. """ + @given(randoms()) def test_interface(self, random): """ @@ -268,7 +223,7 @@ class LeaseMaintenanceServiceTests(TestCase): service = lease_maintenance_service( dummy_maintain_leases, clock, - FilePath(self.useFixture(TempDir()).join(u"last-run")), + FilePath(self.useFixture(TempDir()).join("last-run")), random, ) self.assertThat( @@ -296,7 +251,7 @@ class LeaseMaintenanceServiceTests(TestCase): service = lease_maintenance_service( dummy_maintain_leases, clock, - FilePath(self.useFixture(TempDir()).join(u"last-run")), + FilePath(self.useFixture(TempDir()).join("last-run")), random, mean, range_, @@ -333,7 +288,7 @@ class LeaseMaintenanceServiceTests(TestCase): # Figure out the absolute last run time. last_run = datetime_now - since_last_run - last_run_path = FilePath(self.useFixture(TempDir()).join(u"last-run")) + last_run_path = FilePath(self.useFixture(TempDir()).join("last-run")) last_run_path.setContent(last_run.isoformat()) service = lease_maintenance_service( @@ -359,9 +314,16 @@ class LeaseMaintenanceServiceTests(TestCase): datetime_now + mean + (range_ / 2) - since_last_run, ) - note("mean: {}\nrange: {}\nnow: {}\nlow: {}\nhigh: {}\nsince last: {}".format( - mean, range_, datetime_now, low, high, since_last_run, - )) + note( + "mean: {}\nrange: {}\nnow: {}\nlow: {}\nhigh: {}\nsince last: {}".format( + mean, + range_, + datetime_now, + low, + high, + since_last_run, + ) + ) self.assertThat( datetime.utcfromtimestamp(maintenance_call.getTime()), @@ -379,7 +341,7 @@ class LeaseMaintenanceServiceTests(TestCase): service = lease_maintenance_service( lambda: None, clock, - FilePath(self.useFixture(TempDir()).join(u"last-run")), + FilePath(self.useFixture(TempDir()).join("last-run")), random, ) service.startService() @@ -405,13 +367,14 @@ class LeaseMaintenanceServiceTests(TestCase): When the service runs, it calls the ``maintain_leases`` object. """ leases_maintained_at = [] + def maintain_leases(): leases_maintained_at.append(datetime.utcfromtimestamp(clock.seconds())) service = lease_maintenance_service( maintain_leases, clock, - FilePath(self.useFixture(TempDir()).join(u"last-run")), + FilePath(self.useFixture(TempDir()).join("last-run")), random, ) service.startService() @@ -428,6 +391,7 @@ class VisitStorageIndexesFromRootTests(TestCase): """ Tests for ``visit_storage_indexes_from_root``. """ + @given(node_hierarchies(), clocks()) def test_visits_all_nodes(self, root_node, clock): """ @@ -435,6 +399,7 @@ class VisitStorageIndexesFromRootTests(TestCase): its deepest children. """ visited = [] + def perform_visit(visit_assets): return visit_assets(visited.append) @@ -454,11 +419,7 @@ class VisitStorageIndexesFromRootTests(TestCase): HasLength(len(expected)), AfterPreprocessing( set, - Equals(set( - node.get_storage_index() - for node - in expected - )), + Equals(set(node.get_storage_index() for node in expected)), ), ), ) @@ -468,6 +429,7 @@ class RenewLeasesTests(TestCase): """ Tests for ``renew_leases``. """ + @given(storage_brokers(clocks()), lists(leaf_nodes(), unique=True)) def test_renewed(self, storage_broker, nodes): """ @@ -519,23 +481,20 @@ class RenewLeasesTests(TestCase): succeeded(Always()), ) - relevant_storage_indexes = set( - node.get_storage_index() - for node - in nodes - ) + relevant_storage_indexes = set(node.get_storage_index() for node in nodes) self.assertThat( list( server.get_storage_server() - for server - in storage_broker.get_connected_servers() + for server in storage_broker.get_connected_servers() + ), + AllMatch( + leases_current( + relevant_storage_indexes, + get_now(), + min_lease_remaining, + ) ), - AllMatch(leases_current( - relevant_storage_indexes, - get_now(), - min_lease_remaining, - )), ) @@ -543,6 +502,7 @@ class MaintainLeasesFromRootTests(TestCase): """ Tests for ``maintain_leases_from_root``. """ + @given(storage_brokers(clocks()), node_hierarchies()) def test_renewed(self, storage_broker, root_node): """ @@ -575,22 +535,21 @@ class MaintainLeasesFromRootTests(TestCase): ) relevant_storage_indexes = set( - node.get_storage_index() - for node - in root_node.flatten() + node.get_storage_index() for node in root_node.flatten() ) self.assertThat( list( server.get_storage_server() - for server - in storage_broker.get_connected_servers() + for server in storage_broker.get_connected_servers() + ), + AllMatch( + leases_current( + relevant_storage_indexes, + get_now(), + min_lease_remaining, + ) ), - AllMatch(leases_current( - relevant_storage_indexes, - get_now(), - min_lease_remaining, - )) ) @given(storage_brokers(clocks()), node_hierarchies()) diff --git a/src/_zkapauthorizer/tests/test_matchers.py b/src/_zkapauthorizer/tests/test_matchers.py index 868a1e998bec4a54c093f24637a5a3f8ecee6261..e34bb8ab63f8c98d0dc1b63aaab26d9f45589119 100644 --- a/src/_zkapauthorizer/tests/test_matchers.py +++ b/src/_zkapauthorizer/tests/test_matchers.py @@ -16,27 +16,13 @@ Tests for ``_zkapauthorizer.tests.matchers``. """ -from __future__ import ( - absolute_import, -) +from __future__ import absolute_import -from zope.interface import ( - Interface, - implementer, -) +from testtools import TestCase +from testtools.matchers import Is, Not +from zope.interface import Interface, implementer -from testtools import ( - TestCase, -) -from testtools.matchers import ( - Not, - Is, -) - -from .matchers import ( - Provides, - returns, -) +from .matchers import Provides, returns class IX(Interface): @@ -61,6 +47,7 @@ class ProvidesTests(TestCase): """ Tests for ``Provides``. """ + def test_match(self): """ ``Provides.match`` returns ``None`` when the given object provides all of @@ -86,6 +73,7 @@ class ReturnsTests(TestCase): """ Tests for ``returns``. """ + def test_match(self): """ ``returns(m)`` returns a matcher that matches when the given object diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py index 5af002721ebcc7ce1b7ae45c8b2d0f2e0b045558..bff6a608515b62e430e8d7b3af69849f7d7f1365 100644 --- a/src/_zkapauthorizer/tests/test_model.py +++ b/src/_zkapauthorizer/tests/test_model.py @@ -17,101 +17,69 @@ Tests for ``_zkapauthorizer.model``. """ -from __future__ import ( - absolute_import, -) - -from os import ( - mkdir, -) -from errno import ( - EACCES, -) -from datetime import ( - datetime, - timedelta, -) +from __future__ import absolute_import -from unittest import ( - skipIf, -) +from datetime import datetime, timedelta +from errno import EACCES +from os import mkdir +from unittest import skipIf -from testtools import ( - TestCase, -) -from testtools.matchers import ( - Always, - HasLength, - AfterPreprocessing, - MatchesStructure, - MatchesAll, - Equals, - Raises, - IsInstance, -) -from testtools.twistedsupport import ( - succeeded, -) - -from fixtures import ( - TempDir, -) - -from hypothesis import ( - note, - given, - assume, -) +from fixtures import TempDir +from hypothesis import assume, given, note from hypothesis.stateful import ( RuleBasedStateMachine, - rule, - precondition, invariant, - run_state_machine_as_test + precondition, + rule, + run_state_machine_as_test, ) from hypothesis.strategies import ( - data, booleans, - lists, - tuples, + data, datetimes, - timedeltas, integers, + lists, randoms, + timedeltas, + tuples, ) - -from twisted.python.runtime import ( - platform, +from testtools import TestCase +from testtools.matchers import ( + AfterPreprocessing, + Always, + Equals, + HasLength, + IsInstance, + MatchesAll, + MatchesStructure, + Raises, ) +from testtools.twistedsupport import succeeded +from twisted.python.runtime import platform from ..model import ( - StoreOpenError, + DoubleSpend, + LeaseMaintenanceActivity, NotEnoughTokens, - VoucherStore, - Voucher, Pending, - DoubleSpend, Redeemed, - LeaseMaintenanceActivity, + StoreOpenError, + Voucher, + VoucherStore, memory_connect, ) +from .fixtures import ConfiglessMemoryVoucherStore, TemporaryVoucherStore +from .matchers import raises from .strategies import ( - tahoe_configs, - vouchers, - voucher_objects, - voucher_counters, - random_tokens, - unblinded_tokens, - posix_safe_datetimes, dummy_ristretto_keys, pass_counts, -) -from .fixtures import ( - TemporaryVoucherStore, - ConfiglessMemoryVoucherStore, -) -from .matchers import ( - raises, + posix_safe_datetimes, + random_tokens, + tahoe_configs, + unblinded_tokens, + voucher_counters, + voucher_objects, + vouchers, ) @@ -119,6 +87,7 @@ class VoucherStoreTests(TestCase): """ Tests for ``VoucherStore``. """ + @given(tahoe_configs(), datetimes(), vouchers()) def test_get_missing(self, get_config, now, voucher): """ @@ -131,7 +100,12 @@ class VoucherStoreTests(TestCase): raises(KeyError), ) - @given(tahoe_configs(), vouchers(), lists(random_tokens(), min_size=1, unique=True), datetimes()) + @given( + tahoe_configs(), + vouchers(), + lists(random_tokens(), min_size=1, unique=True), + datetimes(), + ) def test_add(self, get_config, voucher, tokens, now): """ ``VoucherStore.get`` returns a ``Voucher`` representing a voucher @@ -156,15 +130,17 @@ class VoucherStoreTests(TestCase): lists(random_tokens(), min_size=2, unique=True), datetimes(), ) - def test_add_with_distinct_counters(self, get_config, voucher, counters, tokens, now): + def test_add_with_distinct_counters( + self, get_config, voucher, counters, tokens, now + ): """ ``VoucherStore.add`` adds new tokens to the store when passed the same voucher but a different counter value. """ counter_a = counters[0] counter_b = counters[1] - tokens_a = tokens[:len(tokens) / 2] - tokens_b = tokens[len(tokens) / 2:] + tokens_a = tokens[: len(tokens) / 2] + tokens_b = tokens[len(tokens) / 2 :] store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store # We only have to get the expected_tokens value (len(tokens)) right on @@ -185,7 +161,12 @@ class VoucherStoreTests(TestCase): self.assertThat(tokens_a, Equals(added_tokens_a)) self.assertThat(tokens_b, Equals(added_tokens_b)) - @given(tahoe_configs(), vouchers(), datetimes(), lists(random_tokens(), min_size=1, unique=True)) + @given( + tahoe_configs(), + vouchers(), + datetimes(), + lists(random_tokens(), min_size=1, unique=True), + ) def test_add_idempotent(self, get_config, voucher, now, tokens): """ More than one call to ``VoucherStore.add`` with the same argument results @@ -233,14 +214,16 @@ class VoucherStoreTests(TestCase): ``VoucherStore.list`` returns a ``list`` containing a ``Voucher`` object for each voucher previously added. """ - tokens = iter(data.draw( - lists( - random_tokens(), - unique=True, - min_size=len(vouchers), - max_size=len(vouchers), - ), - )) + tokens = iter( + data.draw( + lists( + random_tokens(), + unique=True, + min_size=len(vouchers), + max_size=len(vouchers), + ), + ) + ) store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store for voucher in vouchers: store.add( @@ -252,11 +235,12 @@ class VoucherStoreTests(TestCase): self.assertThat( store.list(), - Equals(list( - Voucher(number, expected_tokens=1, created=now) - for number - in vouchers - )), + Equals( + list( + Voucher(number, expected_tokens=1, created=now) + for number in vouchers + ) + ), ) @skipIf(platform.isWindows(), "Hard to prevent directory creation on Windows") @@ -299,8 +283,9 @@ class VoucherStoreTests(TestCase): ), ) - - @skipIf(platform.isWindows(), "Hard to prevent database from being opened on Windows") + @skipIf( + platform.isWindows(), "Hard to prevent database from being opened on Windows" + ) @given(tahoe_configs(), datetimes()) def test_unopenable_store(self, get_config, now): """ @@ -327,43 +312,37 @@ class VoucherStoreTests(TestCase): ) @given(tahoe_configs(), vouchers(), dummy_ristretto_keys(), datetimes(), data()) - def test_spend_order_equals_backup_order(self, get_config, voucher_value, public_key, now, data): + def test_spend_order_equals_backup_order( + self, get_config, voucher_value, public_key, now, data + ): """ Unblinded tokens returned by ``VoucherStore.backup`` appear in the same order as they are returned by ``VoucherStore.get_unblinded_tokens``. """ backed_up_tokens, spent_tokens, inserted_tokens = self._spend_order_test( - get_config, - voucher_value, - public_key, - now, - data + get_config, voucher_value, public_key, now, data ) self.assertThat( backed_up_tokens, Equals(spent_tokens), ) - @given(tahoe_configs(), vouchers(), dummy_ristretto_keys(), datetimes(), data()) - def test_spend_order_equals_insert_order(self, get_config, voucher_value, public_key, now, data): + def test_spend_order_equals_insert_order( + self, get_config, voucher_value, public_key, now, data + ): """ Unblinded tokens returned by ``VoucherStore.get_unblinded_tokens`` appear in the same order as they were inserted. """ backed_up_tokens, spent_tokens, inserted_tokens = self._spend_order_test( - get_config, - voucher_value, - public_key, - now, - data + get_config, voucher_value, public_key, now, data ) self.assertThat( spent_tokens, Equals(inserted_tokens), ) - def _spend_order_test(self, get_config, voucher_value, public_key, now, data): """ Insert, backup, and extract some tokens. @@ -385,7 +364,9 @@ class VoucherStoreTests(TestCase): store = VoucherStore.from_node_config(config, lambda: now) # Put some tokens in it that we can backup and extract - random_tokens, unblinded_tokens = paired_tokens(data, integers(min_value=1, max_value=5)) + random_tokens, unblinded_tokens = paired_tokens( + data, integers(min_value=1, max_value=5) + ) store.add(voucher_value, len(random_tokens), 0, lambda: random_tokens) store.insert_unblinded_tokens_for_voucher( voucher_value, @@ -401,9 +382,7 @@ class VoucherStoreTests(TestCase): while tokens_remaining > 0: to_spend = data.draw(integers(min_value=1, max_value=tokens_remaining)) extracted_tokens.extend( - token.unblinded_token - for token - in store.get_unblinded_tokens(to_spend) + token.unblinded_token for token in store.get_unblinded_tokens(to_spend) ) tokens_remaining -= to_spend @@ -420,6 +399,7 @@ class UnblindedTokenStateMachine(RuleBasedStateMachine): unblinded tokens in a ``VoucherStore`` - usable, in-use, spent, invalid, etc. """ + def __init__(self, case): super(UnblindedTokenStateMachine, self).__init__() self.case = case @@ -555,12 +535,14 @@ class UnblindedTokenStateMachine(RuleBasedStateMachine): @invariant() def report_state(self): - note("available={} using={} invalid={} spent={}".format( - self.available, - len(self.using), - len(self.invalid), - len(self.spent), - )) + note( + "available={} using={} invalid={} spent={}".format( + self.available, + len(self.using), + len(self.invalid), + len(self.spent), + ) + ) def random_slice(taken_from, random, data): @@ -588,6 +570,7 @@ class UnblindedTokenStateTests(TestCase): """ Glue ``UnblindedTokenStateTests`` into our test runner. """ + def test_states(self): run_state_machine_as_test(lambda: UnblindedTokenStateMachine(self)) @@ -596,6 +579,7 @@ class LeaseMaintenanceTests(TestCase): """ Tests for the lease-maintenance related parts of ``VoucherStore``. """ + @given( tahoe_configs(), posix_safe_datetimes(), @@ -645,9 +629,11 @@ class LeaseMaintenanceTests(TestCase): for (num_passes, trim_size) in sizes: passes_required += num_passes trim_size %= store.pass_value - x.observe([ - num_passes * store.pass_value - trim_size, - ]) + x.observe( + [ + num_passes * store.pass_value - trim_size, + ] + ) now += finish_delay x.finish() finished = now @@ -669,6 +655,7 @@ class VoucherTests(TestCase): """ Tests for ``Voucher``. """ + @given(voucher_objects()) def test_json_roundtrip(self, reference): """ @@ -688,18 +675,22 @@ def paired_tokens(data, sizes=integers(min_value=1, max_value=1000)): :rtype: ([RandomTokens], [UnblindedTokens]) """ num_tokens = data.draw(sizes) - r = data.draw(lists( - random_tokens(), - min_size=num_tokens, - max_size=num_tokens, - unique=True, - )) - u = data.draw(lists( - unblinded_tokens(), - min_size=num_tokens, - max_size=num_tokens, - unique=True, - )) + r = data.draw( + lists( + random_tokens(), + min_size=num_tokens, + max_size=num_tokens, + unique=True, + ) + ) + u = data.draw( + lists( + unblinded_tokens(), + min_size=num_tokens, + max_size=num_tokens, + unique=True, + ) + ) return r, u @@ -707,6 +698,7 @@ class UnblindedTokenStoreTests(TestCase): """ Tests for ``UnblindedToken``-related functionality of ``VoucherStore``. """ + @given( tahoe_configs(), datetimes(), @@ -715,7 +707,9 @@ class UnblindedTokenStoreTests(TestCase): lists(unblinded_tokens(), unique=True), booleans(), ) - def test_unblinded_tokens_without_voucher(self, get_config, now, voucher_value, public_key, unblinded_tokens, completed): + def test_unblinded_tokens_without_voucher( + self, get_config, now, voucher_value, public_key, unblinded_tokens, completed + ): """ Unblinded tokens for a voucher which has not been added to the store cannot be inserted. """ @@ -739,14 +733,18 @@ class UnblindedTokenStoreTests(TestCase): booleans(), data(), ) - def test_unblinded_tokens_round_trip(self, get_config, now, voucher_value, public_key, completed, data): + def test_unblinded_tokens_round_trip( + self, get_config, now, voucher_value, public_key, completed, data + ): """ Unblinded tokens that are added to the store can later be retrieved and counted. """ random_tokens, unblinded_tokens = paired_tokens(data) store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store store.add(voucher_value, len(random_tokens), 0, lambda: random_tokens) - store.insert_unblinded_tokens_for_voucher(voucher_value, public_key, unblinded_tokens, completed, spendable=True) + store.insert_unblinded_tokens_for_voucher( + voucher_value, public_key, unblinded_tokens, completed, spendable=True + ) # All the tokens just inserted should be counted. self.expectThat( @@ -774,7 +772,9 @@ class UnblindedTokenStoreTests(TestCase): integers(min_value=1, max_value=100), data(), ) - def test_mark_vouchers_redeemed(self, get_config, now, voucher_value, public_key, num_tokens, data): + def test_mark_vouchers_redeemed( + self, get_config, now, voucher_value, public_key, num_tokens, data + ): """ The voucher for unblinded tokens that are added to the store is marked as redeemed. @@ -798,16 +798,20 @@ class UnblindedTokenStoreTests(TestCase): store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store store.add(voucher_value, len(random), 0, lambda: random) - store.insert_unblinded_tokens_for_voucher(voucher_value, public_key, unblinded, completed=True, spendable=True) + store.insert_unblinded_tokens_for_voucher( + voucher_value, public_key, unblinded, completed=True, spendable=True + ) loaded_voucher = store.get(voucher_value) self.assertThat( loaded_voucher, MatchesStructure( expected_tokens=Equals(len(random)), - state=Equals(Redeemed( - finished=now, - token_count=num_tokens, - )), + state=Equals( + Redeemed( + finished=now, + token_count=num_tokens, + ) + ), ), ) @@ -817,7 +821,9 @@ class UnblindedTokenStoreTests(TestCase): vouchers(), lists(random_tokens(), min_size=1, unique=True), ) - def test_mark_vouchers_double_spent(self, get_config, now, voucher_value, random_tokens): + def test_mark_vouchers_double_spent( + self, get_config, now, voucher_value, random_tokens + ): """ A voucher which is reported as double-spent is marked in the database as such. @@ -829,9 +835,11 @@ class UnblindedTokenStoreTests(TestCase): self.assertThat( voucher, MatchesStructure( - state=Equals(DoubleSpend( - finished=now, - )), + state=Equals( + DoubleSpend( + finished=now, + ) + ), ), ) @@ -843,7 +851,9 @@ class UnblindedTokenStoreTests(TestCase): integers(min_value=1, max_value=100), data(), ) - def test_mark_spent_vouchers_double_spent(self, get_config, now, voucher_value, public_key, num_tokens, data): + def test_mark_spent_vouchers_double_spent( + self, get_config, now, voucher_value, public_key, num_tokens, data + ): """ A voucher which has already been spent cannot be marked as double-spent. """ @@ -865,7 +875,9 @@ class UnblindedTokenStoreTests(TestCase): ) store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store store.add(voucher_value, len(random), 0, lambda: random) - store.insert_unblinded_tokens_for_voucher(voucher_value, public_key, unblinded, completed=True, spendable=True) + store.insert_unblinded_tokens_for_voucher( + voucher_value, public_key, unblinded, completed=True, spendable=True + ) self.assertThat( lambda: store.mark_voucher_double_spent(voucher_value), raises(ValueError), @@ -895,7 +907,9 @@ class UnblindedTokenStoreTests(TestCase): integers(min_value=1), data(), ) - def test_not_enough_unblinded_tokens(self, get_config, now, voucher_value, public_key, completed, extra, data): + def test_not_enough_unblinded_tokens( + self, get_config, now, voucher_value, public_key, completed, extra, data + ): """ ``get_unblinded_tokens`` raises ``NotEnoughTokens`` if ``count`` is greater than the number of unblinded tokens in the store. diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py index ee8f0016c0a8a197541f35dae6df6f837c1ceeaf..78959a5d37712fd2d57f9eb5ebff7cda75f514bf 100644 --- a/src/_zkapauthorizer/tests/test_plugin.py +++ b/src/_zkapauthorizer/tests/test_plugin.py @@ -16,165 +16,78 @@ Tests for the Tahoe-LAFS plugin. """ -from __future__ import ( - absolute_import, -) +from __future__ import absolute_import -from StringIO import ( - StringIO, -) -from os import ( - makedirs, -) import tempfile -from functools import ( - partial, -) +from functools import partial +from os import makedirs -from fixtures import ( - TempDir, -) - -from testtools import ( - TestCase, +from allmydata.client import config_from_string, create_client_from_config +from allmydata.interfaces import ( + IAnnounceableStorageServer, + IFoolscapStoragePlugin, + IStorageServer, + RIStorageServer, ) +from challenge_bypass_ristretto import SigningKey +from eliot.testing import LoggedMessage +from fixtures import TempDir +from foolscap.broker import Broker +from foolscap.ipb import IReferenceable, IRemotelyCallable +from foolscap.referenceable import LocalReferenceable +from hypothesis import given, settings +from hypothesis.strategies import datetimes, just, sampled_from +from StringIO import StringIO +from testtools import TestCase +from testtools.content import text_content from testtools.matchers import ( + AfterPreprocessing, + AllMatch, Always, Contains, + ContainsDict, Equals, - AfterPreprocessing, - MatchesAll, HasLength, - AllMatch, - ContainsDict, - MatchesStructure, IsInstance, + MatchesAll, + MatchesStructure, ) -from testtools.twistedsupport import ( - succeeded, -) -from testtools.content import ( - text_content, -) -from hypothesis import ( - given, - settings, -) -from hypothesis.strategies import ( - just, - datetimes, - sampled_from, -) -from foolscap.broker import ( - Broker, -) -from foolscap.ipb import ( - IReferenceable, - IRemotelyCallable, -) -from foolscap.referenceable import ( - LocalReferenceable, -) - -from allmydata.interfaces import ( - IFoolscapStoragePlugin, - IAnnounceableStorageServer, - IStorageServer, - RIStorageServer, -) -from allmydata.client import ( - config_from_string, - create_client_from_config, -) - -from eliot.testing import ( - LoggedMessage, -) - -from twisted.python.filepath import ( - FilePath, -) -from twisted.plugin import ( - getPlugins, -) -from twisted.test.proto_helpers import ( - StringTransport, -) -from twisted.internet.task import ( - Clock, -) -from twisted.web.resource import ( - IResource, -) -from twisted.plugins.zkapauthorizer import ( - storage_server, -) - -from challenge_bypass_ristretto import ( - SigningKey, -) - -from ..spending import ( - GET_PASSES, -) - -from ..foolscap import ( - RIPrivacyPassAuthorizedStorageServer, -) -from ..model import ( - NotEnoughTokens, - VoucherStore, -) -from ..controller import ( - IssuerConfigurationMismatch, - PaymentController, - DummyRedeemer, -) -from .._storage_client import ( - IncorrectStorageServerReference, -) - -from ..lease_maintenance import ( - SERVICE_NAME, -) - -from .._plugin import ( - load_signing_key, -) - +from testtools.twistedsupport import succeeded +from twisted.internet.task import Clock +from twisted.plugin import getPlugins +from twisted.python.filepath import FilePath +from twisted.test.proto_helpers import StringTransport +from twisted.web.resource import IResource + +from twisted.plugins.zkapauthorizer import storage_server + +from .._plugin import load_signing_key +from .._storage_client import IncorrectStorageServerReference +from ..controller import DummyRedeemer, IssuerConfigurationMismatch, PaymentController +from ..foolscap import RIPrivacyPassAuthorizedStorageServer +from ..lease_maintenance import SERVICE_NAME +from ..model import NotEnoughTokens, VoucherStore +from ..spending import GET_PASSES +from .eliot import capture_logging +from .foolscap import DummyReferenceable, LocalRemote, get_anonymous_storage_server +from .matchers import Provides, raises from .strategies import ( - minimal_tahoe_configs, - tahoe_configs, - client_dummyredeemer_configurations, - server_configurations, announcements, - vouchers, - storage_indexes, - lease_renew_secrets, + client_dummyredeemer_configurations, + dummy_ristretto_keys, lease_cancel_secrets, - sharenum_sets, - sizes, + lease_renew_secrets, + minimal_tahoe_configs, pass_counts, ristretto_signing_keys, - dummy_ristretto_keys, -) -from .matchers import ( - Provides, - raises, -) - -from .foolscap import ( - LocalRemote, - get_anonymous_storage_server, - DummyReferenceable, -) - -from .eliot import ( - capture_logging, + server_configurations, + sharenum_sets, + sizes, + storage_indexes, + tahoe_configs, + vouchers, ) - - SIGNING_KEY_PATH = FilePath(__file__).sibling(u"testing-signing.key") @@ -184,11 +97,11 @@ def get_rref(interface=None): return LocalRemote(DummyReferenceable(interface)) - class GetRRefTests(TestCase): """ Tests for ``get_rref``. """ + def test_localremote(self): """ ``get_rref`` returns an instance of ``LocalRemote``. @@ -210,7 +123,9 @@ class GetRRefTests(TestCase): AfterPreprocessing( lambda ref: ref.tracker, MatchesStructure( - interfaceName=Equals(RIPrivacyPassAuthorizedStorageServer.__remote_name__), + interfaceName=Equals( + RIPrivacyPassAuthorizedStorageServer.__remote_name__ + ), ), ), ) @@ -237,6 +152,7 @@ class PluginTests(TestCase): """ Tests for ``twisted.plugins.zkapauthorizer.storage_server``. """ + def test_discoverable(self): """ The plugin can be discovered. @@ -246,7 +162,6 @@ class PluginTests(TestCase): Contains(storage_server), ) - def test_provides_interface(self): """ ``storage_server`` provides ``IFoolscapStoragePlugin``. @@ -257,12 +172,12 @@ class PluginTests(TestCase): ) - class ServerPluginTests(TestCase): """ Tests for the plugin's implementation of ``IFoolscapStoragePlugin.get_storage_server``. """ + @given(server_configurations(SIGNING_KEY_PATH)) def test_returns_announceable(self, configuration): """ @@ -278,7 +193,6 @@ class ServerPluginTests(TestCase): succeeded(Provides([IAnnounceableStorageServer])), ) - @given(server_configurations(SIGNING_KEY_PATH)) def test_returns_referenceable(self, configuration): """ @@ -323,7 +237,6 @@ class ServerPluginTests(TestCase): ), ) - @given(server_configurations(SIGNING_KEY_PATH)) def test_returns_hashable(self, configuration): """ @@ -352,15 +265,21 @@ class ServerPluginTests(TestCase): tahoe_configs_with_dummy_redeemer = tahoe_configs(client_dummyredeemer_configurations()) -tahoe_configs_with_mismatched_issuer = minimal_tahoe_configs({ - u"privatestorageio-zkapauthz-v1": just({u"ristretto-issuer-root-url": u"https://another-issuer.example.invalid/"}), -}) +tahoe_configs_with_mismatched_issuer = minimal_tahoe_configs( + { + u"privatestorageio-zkapauthz-v1": just( + {u"ristretto-issuer-root-url": u"https://another-issuer.example.invalid/"} + ), + } +) + class ClientPluginTests(TestCase): """ Tests for the plugin's implementation of ``IFoolscapStoragePlugin.get_storage_client``. """ + @given(tahoe_configs(), announcements()) def test_interface(self, get_config, announcement): """ @@ -384,7 +303,6 @@ class ClientPluginTests(TestCase): Provides([IStorageServer]), ) - @given(tahoe_configs_with_mismatched_issuer, announcements()) def test_mismatched_ristretto_issuer(self, config_text, announcement): """ @@ -420,7 +338,6 @@ class ClientPluginTests(TestCase): raises(IssuerConfigurationMismatch), ) - @given( tahoe_configs(), announcements(), @@ -431,15 +348,14 @@ class ClientPluginTests(TestCase): sizes(), ) def test_mismatch_storage_server_furl( - self, - get_config, - announcement, - storage_index, - renew_secret, - cancel_secret, - sharenums, - size, - + self, + get_config, + announcement, + storage_index, + renew_secret, + cancel_secret, + sharenums, + size, ): """ If the ``get_rref`` passed to ``get_storage_client`` returns a reference @@ -484,14 +400,14 @@ class ClientPluginTests(TestCase): ) @capture_logging(lambda self, logger: logger.validate()) def test_unblinded_tokens_spent( - self, - logger, - get_config, - now, - announcement, - voucher, - num_passes, - public_key, + self, + logger, + get_config, + now, + announcement, + voucher, + num_passes, + public_key, ): """ The ``ZKAPAuthorizerStorageServer`` returned by ``get_storage_client`` @@ -548,10 +464,12 @@ class ClientPluginTests(TestCase): AllMatch( AfterPreprocessing( lambda logged_message: logged_message.message, - ContainsDict({ - u"message": Equals(u"request binding message"), - u"count": Equals(num_passes), - }), + ContainsDict( + { + u"message": Equals(u"request binding message"), + u"count": Equals(num_passes), + } + ), ), ), ), @@ -563,6 +481,7 @@ class ClientResourceTests(TestCase): Tests for the plugin's implementation of ``IFoolscapStoragePlugin.get_client_resource``. """ + @given(tahoe_configs()) def test_interface(self, get_config): """ @@ -617,6 +536,7 @@ class LeaseMaintenanceServiceTests(TestCase): """ Tests for the plugin's initialization of the lease maintenance service. """ + def _created_test(self, get_config, servers_yaml, rootcap): original_tempdir = tempfile.tempdir @@ -654,7 +574,7 @@ class LeaseMaintenanceServiceTests(TestCase): # suite if we don't clean it up. We can't do this with a tearDown # or a fixture or an addCleanup because hypothesis doesn't run any # of those at the right time. :/ - tempfile.tempdir = original_tempdir + tempfile.tempdir = original_tempdir @settings( deadline=None, @@ -671,7 +591,6 @@ class LeaseMaintenanceServiceTests(TestCase): """ return self._created_test(get_config, servers_yaml, rootcap=True) - @settings( deadline=None, ) @@ -691,6 +610,7 @@ class LoadSigningKeyTests(TestCase): """ Tests for ``load_signing_key``. """ + @given(ristretto_signing_keys()) def test_valid(self, key_bytes): """ diff --git a/src/_zkapauthorizer/tests/test_pricecalculator.py b/src/_zkapauthorizer/tests/test_pricecalculator.py index 25eaa40d09102b66ffbb592d590497c33d1c8517..baadd9119d73a37988d90baa5bdb62f106405e73 100644 --- a/src/_zkapauthorizer/tests/test_pricecalculator.py +++ b/src/_zkapauthorizer/tests/test_pricecalculator.py @@ -17,48 +17,25 @@ Tests for ``_zkapauthorizer.pricecalculator``. """ -from functools import ( - partial, -) - -from testtools import ( - TestCase, -) -from testtools.matchers import ( - Equals, - GreaterThan, - IsInstance, - MatchesAll, -) - -from hypothesis import ( - given, -) - -from hypothesis.strategies import ( - integers, - lists, - tuples, -) - -from ..pricecalculator import ( - PriceCalculator, -) - -from .strategies import ( - sizes, - share_parameters, -) -from .matchers import ( - greater_or_equal, -) +from functools import partial + +from hypothesis import given +from hypothesis.strategies import integers, lists, tuples +from testtools import TestCase +from testtools.matchers import Equals, GreaterThan, IsInstance, MatchesAll + +from ..pricecalculator import PriceCalculator +from .matchers import greater_or_equal +from .strategies import share_parameters, sizes file_sizes = lists(sizes(), min_size=1) + class PriceCalculatorTests(TestCase): """ Tests for ``PriceCalculator``. """ + @given( integers(min_value=1), integers(min_value=1), @@ -103,7 +80,6 @@ class PriceCalculatorTests(TestCase): greater_or_equal(more_needed_price), ) - @given( integers(min_value=1, max_value=127), integers(min_value=1, max_value=127), diff --git a/src/_zkapauthorizer/tests/test_private.py b/src/_zkapauthorizer/tests/test_private.py index 9382eb54021adea9a893b1b412c065398a4f0704..568cc1eb1baf613c17ee3336874b08b9aed4b17c 100644 --- a/src/_zkapauthorizer/tests/test_private.py +++ b/src/_zkapauthorizer/tests/test_private.py @@ -9,65 +9,39 @@ Tests for ``_zkapauthorizer.private``. """ -from __future__ import ( - print_function, - unicode_literals, - absolute_import, - division, -) +from __future__ import absolute_import, division, print_function, unicode_literals -from testtools import ( - TestCase, -) -from testtools.matchers import ( - Equals, -) -from testtools.twistedsupport import ( - succeeded, -) +from allmydata.test.web.matchers import has_response_code +from testtools import TestCase +from testtools.matchers import Equals +from testtools.twistedsupport import succeeded +from treq.client import HTTPClient +from treq.testing import RequestTraversalAgent +from twisted.web.http import NOT_FOUND, UNAUTHORIZED +from twisted.web.http_headers import Headers +from twisted.web.resource import Resource -from twisted.web.http import ( - UNAUTHORIZED, - NOT_FOUND, -) -from twisted.web.http_headers import ( - Headers, -) -from twisted.web.resource import ( - Resource, -) +from ..private import SCHEME, create_private_tree -from treq.client import ( - HTTPClient, -) -from treq.testing import ( - RequestTraversalAgent, -) - -from ..private import ( - SCHEME, - create_private_tree, -) - -from allmydata.test.web.matchers import ( - has_response_code, -) class PrivacyTests(TestCase): """ Tests for the privacy features of the resources created by ``create_private_tree``. """ + def setUp(self): self.token = b"abcdef" self.resource = create_private_tree(lambda: self.token, Resource()) self.agent = RequestTraversalAgent(self.resource) - self.client = HTTPClient(self.agent) + self.client = HTTPClient(self.agent) return super(PrivacyTests, self).setUp() def _authorization(self, scheme, value): - return Headers({ - u"authorization": [u"{} {}".format(scheme, value)], - }) + return Headers( + { + "authorization": ["{} {}".format(scheme, value)], + } + ) def test_unauthorized(self): """ @@ -86,7 +60,7 @@ class PrivacyTests(TestCase): self.assertThat( self.client.head( b"http:///foo/bar", - headers=self._authorization(u"basic", self.token), + headers=self._authorization("basic", self.token), ), succeeded(has_response_code(Equals(UNAUTHORIZED))), ) @@ -99,7 +73,7 @@ class PrivacyTests(TestCase): self.assertThat( self.client.head( b"http:///foo/bar", - headers=self._authorization(SCHEME, u"foo bar"), + headers=self._authorization(SCHEME, "foo bar"), ), succeeded(has_response_code(Equals(UNAUTHORIZED))), ) diff --git a/src/_zkapauthorizer/tests/test_schema.py b/src/_zkapauthorizer/tests/test_schema.py index da1af22218d7431852e341f6e7a7d1106d65f6d1..1c53556018b064b4b47855d03242607ab8dd3aba 100644 --- a/src/_zkapauthorizer/tests/test_schema.py +++ b/src/_zkapauthorizer/tests/test_schema.py @@ -17,20 +17,13 @@ Tests for ``_zkapauthorizer.schema``. """ -from __future__ import ( - absolute_import, -) +from __future__ import absolute_import -from testtools import ( - TestCase, -) -from testtools.matchers import ( - Equals, -) +from testtools import TestCase +from testtools.matchers import Equals + +from ..schema import _UPGRADES -from ..schema import ( - _UPGRADES, -) class UpgradeTests(TestCase): def test_consistency(self): diff --git a/src/_zkapauthorizer/tests/test_spending.py b/src/_zkapauthorizer/tests/test_spending.py index bb52eff98f14da6c06e63ed61aa77fbacb228321..6833e0895e5d8f4ec38310eec5c25456ccc8e601 100644 --- a/src/_zkapauthorizer/tests/test_spending.py +++ b/src/_zkapauthorizer/tests/test_spending.py @@ -16,50 +16,30 @@ Tests for ``_zkapauthorizer.spending``. """ -from testtools import ( - TestCase, -) +from hypothesis import given +from hypothesis.strategies import data, integers, randoms +from testtools import TestCase from testtools.matchers import ( + AfterPreprocessing, Always, Equals, + HasLength, MatchesAll, MatchesStructure, - HasLength, - AfterPreprocessing, -) -from testtools.twistedsupport import ( - succeeded, ) +from testtools.twistedsupport import succeeded -from hypothesis import ( - given, -) -from hypothesis.strategies import ( - integers, - randoms, - data, -) +from ..spending import IPassGroup, SpendingController +from .fixtures import ConfiglessMemoryVoucherStore +from .matchers import Provides +from .strategies import pass_counts, posix_safe_datetimes, vouchers -from .strategies import ( - vouchers, - pass_counts, - posix_safe_datetimes, -) -from .matchers import ( - Provides, -) -from .fixtures import ( - ConfiglessMemoryVoucherStore, -) -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): """ @@ -94,14 +74,14 @@ class PassGroupTests(TestCase): ) def _test_token_group_operation( - self, - operation, - matches_tokens, - voucher, - num_passes, - now, - random, - data, + self, + operation, + matches_tokens, + voucher, + num_passes, + now, + random, + data, ): configless = self.useFixture( ConfiglessMemoryVoucherStore( @@ -143,6 +123,7 @@ class PassGroupTests(TestCase): Passes in a group can be marked as successfully spent to prevent them from being re-used by a future ``get`` call. """ + def matches_tokens(num_passes, group): return AfterPreprocessing( # The use of `backup` here to check is questionable. TODO: @@ -150,6 +131,7 @@ class PassGroupTests(TestCase): lambda store: store.backup()[u"unblinded-tokens"], HasLength(num_passes - len(group.passes)), ) + return self._test_token_group_operation( lambda group: group.mark_spent(), matches_tokens, @@ -166,6 +148,7 @@ class PassGroupTests(TestCase): Passes in a group can be marked as invalid to prevent them from being re-used by a future ``get`` call. """ + def matches_tokens(num_passes, group): return AfterPreprocessing( # The use of `backup` here to check is questionable. TODO: @@ -173,6 +156,7 @@ class PassGroupTests(TestCase): lambda store: store.backup()[u"unblinded-tokens"], HasLength(num_passes - len(group.passes)), ) + return self._test_token_group_operation( lambda group: group.mark_invalid(u"reason"), matches_tokens, @@ -189,12 +173,14 @@ class PassGroupTests(TestCase): Passes in a group can be reset to allow them to be re-used by a future ``get`` call. """ + def matches_tokens(num_passes, group): return AfterPreprocessing( # They've been reset so we should be able to re-get them. lambda store: store.get_unblinded_tokens(len(group.passes)), Equals(group.unblinded_tokens), ) + return self._test_token_group_operation( lambda group: group.reset(), matches_tokens, diff --git a/src/_zkapauthorizer/tests/test_storage_client.py b/src/_zkapauthorizer/tests/test_storage_client.py index c697090885784e259258b873c0ce68e340e190f2..1075884f570d7b15b220f5d33843c5105ecb522f 100644 --- a/src/_zkapauthorizer/tests/test_storage_client.py +++ b/src/_zkapauthorizer/tests/test_storage_client.py @@ -16,91 +16,48 @@ Tests for ``_zkapauthorizer._storage_client``. """ -from __future__ import ( - division, -) +from __future__ import division -from functools import ( - partial, -) +from functools import partial -from testtools import ( - TestCase, -) +from allmydata.client import config_from_string +from hypothesis import given +from hypothesis.strategies import integers, sampled_from, sets +from testtools import TestCase from testtools.matchers import ( + AfterPreprocessing, + AllMatch, Always, - Is, Equals, - AfterPreprocessing, - MatchesStructure, HasLength, - MatchesAll, - AllMatch, + Is, IsInstance, + MatchesAll, + MatchesStructure, ) -from testtools.twistedsupport import ( - succeeded, - failed, -) - -from hypothesis import ( - given, -) -from hypothesis.strategies import ( - sampled_from, - integers, - sets, -) - -from twisted.internet.defer import ( - succeed, - fail, -) - -from allmydata.client import ( - config_from_string, -) +from testtools.twistedsupport import failed, succeeded +from twisted.internet.defer import fail, succeed - -from ..api import ( - MorePassesRequired, -) -from ..model import ( - NotEnoughTokens, -) +from .._storage_client import call_with_passes +from .._storage_server import _ValidationResult +from ..api import MorePassesRequired +from ..model import NotEnoughTokens from ..storage_common import ( + get_configured_allowed_public_keys, + get_configured_pass_value, get_configured_shares_needed, get_configured_shares_total, - get_configured_pass_value, - get_configured_allowed_public_keys, -) -from .._storage_client import ( - call_with_passes, -) - -from .._storage_server import ( - _ValidationResult, -) -from .matchers import ( - even, - odd, - raises, -) -from .strategies import ( - pass_counts, - dummy_ristretto_keys, -) -from .storage_common import ( - pass_factory, - integer_passes, ) - +from .matchers import even, odd, raises +from .storage_common import integer_passes, pass_factory +from .strategies import dummy_ristretto_keys, pass_counts class GetConfiguredValueTests(TestCase): """ Tests for helpers for reading certain configuration values. """ + @given(integers(min_value=1, max_value=255)) def test_get_configured_shares_needed(self, expected): """ @@ -115,7 +72,9 @@ class GetConfiguredValueTests(TestCase): shares.needed = {} shares.happy = 5 shares.total = 10 -""".format(expected), +""".format( + expected + ), ) self.assertThat( @@ -137,7 +96,9 @@ shares.total = 10 shares.needed = 5 shares.happy = 5 shares.total = {} -""".format(expected), +""".format( + expected + ), ) self.assertThat( @@ -163,7 +124,9 @@ shares.total = 10 [storageclient.plugins.privatestorageio-zkapauthz-v1] pass-value={} -""".format(expected), +""".format( + expected + ), ) self.assertThat( @@ -189,7 +152,9 @@ shares.total = 10 [storageclient.plugins.privatestorageio-zkapauthz-v1] allowed-public-keys = {} -""".format(",".join(expected)), +""".format( + ",".join(expected) + ), ) self.assertThat( @@ -202,6 +167,7 @@ class CallWithPassesTests(TestCase): """ Tests for ``call_with_passes``. """ + @given(pass_counts()) def test_success_result(self, num_passes): """ @@ -321,7 +287,9 @@ class CallWithPassesTests(TestCase): def reject_even_pass_values(group): passes = group.passes good_passes = list(idx for (idx, p) in enumerate(passes) if p % 2) - bad_passes = list(idx for (idx, p) in enumerate(passes) if idx not in good_passes) + bad_passes = list( + idx for (idx, p) in enumerate(passes) if idx not in good_passes + ) if len(good_passes) < num_passes: _ValidationResult( valid=good_passes, @@ -463,12 +431,15 @@ class CallWithPassesTests(TestCase): ), ) + def reset(group): group.reset() + def spend(group): group.mark_spent() + def invalidate(group): group.mark_invalid(u"reason") @@ -480,6 +451,7 @@ class PassFactoryTests(TestCase): It is unfortunate that this isn't the same test suite as ``test_spending.PassGroupTests``. """ + @given(pass_counts(), pass_counts()) def test_returned_passes_reused(self, num_passes_a, num_passes_b): """ diff --git a/src/_zkapauthorizer/tests/test_storage_protocol.py b/src/_zkapauthorizer/tests/test_storage_protocol.py index afe963c24d3f5e932fc1871d5122fe3dbe2eb91c..f9ad02602a1d8a4567b371a7c48463c052489e35 100644 --- a/src/_zkapauthorizer/tests/test_storage_protocol.py +++ b/src/_zkapauthorizer/tests/test_storage_protocol.py @@ -16,121 +16,72 @@ Tests for communication between the client and server components. """ -from __future__ import ( - absolute_import, -) - -from fixtures import ( - MonkeyPatch, -) -from testtools import ( - TestCase, -) +from __future__ import absolute_import + +from allmydata.storage.common import storage_index_to_dir +from challenge_bypass_ristretto import random_signing_key +from fixtures import MonkeyPatch +from foolscap.referenceable import LocalReferenceable +from hypothesis import assume, given +from hypothesis.strategies import data as data_strategy +from hypothesis.strategies import integers, lists, sets, tuples +from testtools import TestCase from testtools.matchers import ( + AfterPreprocessing, Always, Equals, HasLength, IsInstance, - AfterPreprocessing, MatchesStructure, raises, ) -from testtools.twistedsupport import ( - succeeded, - failed, -) -from testtools.twistedsupport._deferred import ( - # I'd rather use https://twistedmatrix.com/trac/ticket/8900 but efforts - # there appear to have stalled. - extract_result, -) - -from hypothesis import ( - given, - assume, -) -from hypothesis.strategies import ( - sets, - lists, - tuples, - integers, - data as data_strategy, -) - -from twisted.python.runtime import ( - platform, -) -from twisted.python.filepath import ( - FilePath, -) -from twisted.internet.task import ( - Clock, -) - -from foolscap.referenceable import ( - LocalReferenceable, -) - -from challenge_bypass_ristretto import ( - random_signing_key, -) - -from allmydata.storage.common import ( - storage_index_to_dir, -) +from testtools.twistedsupport import failed, succeeded -from .common import ( - skipIf, -) +# I'd rather use https://twistedmatrix.com/trac/ticket/8900 but efforts +# there appear to have stalled. +from testtools.twistedsupport._deferred import extract_result +from twisted.internet.task import Clock +from twisted.python.filepath import FilePath +from twisted.python.runtime import platform -from .strategies import ( - storage_indexes, - lease_renew_secrets, - lease_cancel_secrets, - write_enabler_secrets, - share_versions, - sharenums, - sharenum_sets, - sizes, - slot_test_and_write_vectors_for_shares, - posix_timestamps, - # Not really a strategy... - bytes_for_share, -) -from .matchers import ( - matches_version_dictionary, -) -from .fixtures import ( - AnonymousStorageServer, -) -from .storage_common import ( - LEASE_INTERVAL, - cleanup_storage_server, - write_toy_shares, - whitebox_write_sparse_share, - get_passes, - privacypass_passes, - pass_factory, -) -from .foolscap import ( - LocalRemote, -) +from .._storage_client import _encode_passes from ..api import ( MorePassesRequired, - ZKAPAuthorizerStorageServer, ZKAPAuthorizerStorageClient, + ZKAPAuthorizerStorageServer, ) +from ..foolscap import ShareStat from ..storage_common import ( - slot_testv_and_readv_and_writev_message, allocate_buckets_message, get_implied_data_length, required_passes, + slot_testv_and_readv_and_writev_message, ) -from .._storage_client import ( - _encode_passes, +from .common import skipIf +from .fixtures import AnonymousStorageServer +from .foolscap import LocalRemote +from .matchers import matches_version_dictionary +from .storage_common import ( + LEASE_INTERVAL, + cleanup_storage_server, + get_passes, + pass_factory, + privacypass_passes, + whitebox_write_sparse_share, + write_toy_shares, ) -from ..foolscap import ( - ShareStat, +from .strategies import bytes_for_share # Not really a strategy... +from .strategies import ( + lease_cancel_secrets, + lease_renew_secrets, + posix_timestamps, + share_versions, + sharenum_sets, + sharenums, + sizes, + slot_test_and_write_vectors_for_shares, + storage_indexes, + write_enabler_secrets, ) @@ -138,6 +89,7 @@ class RequiredPassesTests(TestCase): """ Tests for ``required_passes``. """ + @given(integers(min_value=1), sets(integers(min_value=0))) def test_incorrect_types(self, bytes_per_pass, share_sizes): """ @@ -160,11 +112,7 @@ class RequiredPassesTests(TestCase): """ actual = required_passes( bytes_per_pass, - list( - passes * bytes_per_pass - for passes - in expected_per_share - ), + list(passes * bytes_per_pass for passes in expected_per_share), ) self.assertThat( actual, @@ -191,13 +139,16 @@ class ShareTests(TestCase): :ivar pass_factory: An object which is responsible for creating passes which are used by these tests. """ + pass_value = 128 * 1024 def setUp(self): super(ShareTests, self).setUp() self.canary = LocalReferenceable(None) self.signing_key = random_signing_key() - self.pass_factory = pass_factory(get_passes=privacypass_passes(self.signing_key)) + self.pass_factory = pass_factory( + get_passes=privacypass_passes(self.signing_key) + ) self.clock = Clock() self.anonymous_storage_server = self.useFixture( @@ -246,7 +197,9 @@ class ShareTests(TestCase): size=sizes(), data=data_strategy(), ) - def test_rejected_passes_reported(self, storage_index, renew_secret, cancel_secret, sharenums, size, data): + def test_rejected_passes_reported( + self, storage_index, renew_secret, cancel_secret, sharenums, size, data + ): """ Any passes rejected by the storage server are reported with a ``MorePassesRequired`` exception sent to the client. @@ -301,11 +254,7 @@ class ShareTests(TestCase): # it. self.local_remote_server.callRemote( "allocate_buckets", - list( - pass_.pass_text.encode("ascii") - for pass_ - in all_passes - ), + list(pass_.pass_text.encode("ascii") for pass_ in all_passes), storage_index, renew_secret, cancel_secret, @@ -334,7 +283,9 @@ class ShareTests(TestCase): sharenums=sharenum_sets(), size=sizes(), ) - def test_create_immutable(self, storage_index, renew_secret, cancel_secret, sharenums, size): + def test_create_immutable( + self, storage_index, renew_secret, cancel_secret, sharenums, size + ): """ Immutable share data created using *allocate_buckets* and methods of the resulting buckets can be read back using *get_buckets* and methods of @@ -399,14 +350,12 @@ class ShareTests(TestCase): MatchesStructure( issued=HasLength(anticipated_passes), spent=HasLength(anticipated_passes), - returned=HasLength(0), in_use=HasLength(0), invalid=HasLength(0), ), ) - @given( storage_index=storage_indexes(), renew_secret=lease_renew_secrets(), @@ -416,13 +365,13 @@ class ShareTests(TestCase): size=sizes(), ) def test_shares_already_exist( - self, - storage_index, - renew_secret, - cancel_secret, - existing_sharenums, - additional_sharenums, - size, + self, + storage_index, + renew_secret, + cancel_secret, + existing_sharenums, + additional_sharenums, + size, ): """ When the remote *allocate_buckets* implementation reports that shares @@ -493,7 +442,6 @@ class ShareTests(TestCase): issued=HasLength(anticipated_passes), spent=HasLength(expected_spent_passes), returned=HasLength(expected_returned_passes), - in_use=HasLength(0), invalid=HasLength(0), ), @@ -506,7 +454,9 @@ class ShareTests(TestCase): sharenums=sharenum_sets(), size=sizes(), ) - def test_add_lease(self, storage_index, renew_secrets, cancel_secret, sharenums, size): + def test_add_lease( + self, storage_index, renew_secrets, cancel_secret, sharenums, size + ): """ A lease can be added to an existing immutable share. """ @@ -541,7 +491,9 @@ class ShareTests(TestCase): leases = list(self.anonymous_storage_server.get_leases(storage_index)) self.assertThat(leases, HasLength(2)) - def _stat_shares_immutable_test(self, storage_index, sharenum, size, when, leases, write_shares): + def _stat_shares_immutable_test( + self, storage_index, sharenum, size, when, leases, write_shares + ): # Hypothesis causes our storage server to be used many times. Clean # up between iterations. cleanup_storage_server(self.anonymous_storage_server) @@ -579,12 +531,14 @@ class ShareTests(TestCase): finally: patch.cleanUp() - expected = [{ - sharenum: ShareStat( - size=size, - lease_expiration=int(self.clock.seconds() + LEASE_INTERVAL), - ), - }] + expected = [ + { + sharenum: ShareStat( + size=size, + lease_expiration=int(self.clock.seconds() + LEASE_INTERVAL), + ), + } + ] self.assertThat( self.client.stat_shares([storage_index]), succeeded(Equals(expected)), @@ -599,7 +553,9 @@ class ShareTests(TestCase): when=posix_timestamps(), leases=lists(lease_renew_secrets(), unique=True), ) - def test_stat_shares_immutable(self, storage_index, renew_secret, cancel_secret, sharenum, size, when, leases): + def test_stat_shares_immutable( + self, storage_index, renew_secret, cancel_secret, sharenum, size, when, leases + ): """ Size and lease information about immutable shares can be retrieved from a storage server. @@ -629,7 +585,9 @@ class ShareTests(TestCase): leases=lists(lease_renew_secrets(), unique=True, min_size=1), version=share_versions(), ) - def test_stat_shares_immutable_wrong_version(self, storage_index, sharenum, size, when, leases, version): + def test_stat_shares_immutable_wrong_version( + self, storage_index, sharenum, size, when, leases, version + ): """ If a share file with an unexpected version is found, ``stat_shares`` declines to offer a result (by raising ``ValueError``). @@ -674,7 +632,9 @@ class ShareTests(TestCase): # Encode our knowledge of the share header format and size right here... position=integers(min_value=0, max_value=11), ) - def test_stat_shares_truncated_file(self, storage_index, sharenum, size, when, version, position): + def test_stat_shares_truncated_file( + self, storage_index, sharenum, size, when, version, position + ): """ If a share file is truncated in the middle of the header, ``stat_shares`` declines to offer a result (by raising @@ -713,8 +673,10 @@ class ShareTests(TestCase): ), ) - - @skipIf(platform.isWindows(), "Creating large files on Windows (no sparse files) is too slow") + @skipIf( + platform.isWindows(), + "Creating large files on Windows (no sparse files) is too slow", + ) @given( storage_index=storage_indexes(), sharenum=sharenums(), @@ -722,7 +684,9 @@ class ShareTests(TestCase): when=posix_timestamps(), leases=lists(lease_renew_secrets(), unique=True, min_size=1), ) - def test_stat_shares_immutable_large(self, storage_index, sharenum, size, when, leases): + def test_stat_shares_immutable_large( + self, storage_index, sharenum, size, when, leases + ): """ Size and lease information about very large immutable shares can be retrieved from a storage server. @@ -731,6 +695,7 @@ class ShareTests(TestCase): share placement and layout. This is necessary to avoid having to write real multi-gigabyte files to exercise the behavior. """ + def write_shares(storage_server, storage_index, sharenums, size, canary): sharedir = FilePath(storage_server.sharedir).preauthChild( # storage_index_to_dir likes to return multiple segments @@ -768,7 +733,9 @@ class ShareTests(TestCase): test_and_write_vectors_for_shares=slot_test_and_write_vectors_for_shares(), when=posix_timestamps(), ) - def test_stat_shares_mutable(self, storage_index, secrets, test_and_write_vectors_for_shares, when): + def test_stat_shares_mutable( + self, storage_index, secrets, test_and_write_vectors_for_shares, when + ): """ Size and lease information about mutable shares can be retrieved from a storage server. @@ -789,8 +756,7 @@ class ShareTests(TestCase): secrets=secrets, tw_vectors={ k: v.for_call() - for (k, v) - in test_and_write_vectors_for_shares.items() + for (k, v) in test_and_write_vectors_for_shares.items() }, r_vector=[], ), @@ -803,23 +769,23 @@ class ShareTests(TestCase): u"Server rejected a write to a new mutable slot", ) - expected = [{ - sharenum: ShareStat( - size=get_implied_data_length( - vectors.write_vector, - vectors.new_length, - ), - lease_expiration=int(self.clock.seconds() + LEASE_INTERVAL), - ) - for (sharenum, vectors) - in test_and_write_vectors_for_shares.items() - }] + expected = [ + { + sharenum: ShareStat( + size=get_implied_data_length( + vectors.write_vector, + vectors.new_length, + ), + lease_expiration=int(self.clock.seconds() + LEASE_INTERVAL), + ) + for (sharenum, vectors) in test_and_write_vectors_for_shares.items() + } + ] self.assertThat( self.client.stat_shares([storage_index]), succeeded(Equals(expected)), ) - @skipIf( platform.isWindows(), "StorageServer fails to create necessary directory for corruption advisories in Windows.", @@ -831,7 +797,9 @@ class ShareTests(TestCase): sharenum=sharenums(), size=sizes(), ) - def test_advise_corrupt_share(self, storage_index, renew_secret, cancel_secret, sharenum, size): + def test_advise_corrupt_share( + self, storage_index, renew_secret, cancel_secret, sharenum, size + ): """ An advisory of corruption in a share can be sent to the server. """ @@ -873,7 +841,9 @@ class ShareTests(TestCase): ), test_and_write_vectors_for_shares=slot_test_and_write_vectors_for_shares(), ) - def test_create_mutable(self, storage_index, secrets, test_and_write_vectors_for_shares): + def test_create_mutable( + self, storage_index, secrets, test_and_write_vectors_for_shares + ): """ Mutable share data written using *slot_testv_and_readv_and_writev* can be read back as-written and without spending any more passes. @@ -888,8 +858,7 @@ class ShareTests(TestCase): secrets=secrets, tw_vectors={ k: v.for_call() - for (k, v) - in test_and_write_vectors_for_shares.items() + for (k, v) in test_and_write_vectors_for_shares.items() }, r_vector=[], ), @@ -906,7 +875,9 @@ class ShareTests(TestCase): ) # Now we can read it back without spending any more passes. before_passes = len(self.pass_factory.issued) - assert_read_back_data(self, storage_index, secrets, test_and_write_vectors_for_shares) + assert_read_back_data( + self, storage_index, secrets, test_and_write_vectors_for_shares + ) after_passes = len(self.pass_factory.issued) self.assertThat( before_passes, @@ -922,7 +893,9 @@ class ShareTests(TestCase): ), test_and_write_vectors_for_shares=slot_test_and_write_vectors_for_shares(), ) - def test_mutable_rewrite_preserves_lease(self, storage_index, secrets, test_and_write_vectors_for_shares): + def test_mutable_rewrite_preserves_lease( + self, storage_index, secrets, test_and_write_vectors_for_shares + ): """ When mutable share data is rewritten using *slot_testv_and_readv_and_writev* any leases on the corresponding slot @@ -935,8 +908,9 @@ class ShareTests(TestCase): def leases(): return list( lease.to_mutable_data() - for lease - in self.anonymous_storage_server.get_slot_leases(storage_index) + for lease in self.anonymous_storage_server.get_slot_leases( + storage_index + ) ) def write(): @@ -945,8 +919,7 @@ class ShareTests(TestCase): secrets=secrets, tw_vectors={ k: v.for_call() - for (k, v) - in test_and_write_vectors_for_shares.items() + for (k, v) in test_and_write_vectors_for_shares.items() }, r_vector=[], ) @@ -985,15 +958,15 @@ class ShareTests(TestCase): test_and_write_vectors_for_shares=slot_test_and_write_vectors_for_shares(), ) def test_mutable_rewrite_renews_expired_lease( - self, - storage_index, - when, - sharenum, - size, - write_enabler, - renew_secret, - cancel_secret, - test_and_write_vectors_for_shares, + self, + storage_index, + when, + sharenum, + size, + write_enabler, + renew_secret, + cancel_secret, + test_and_write_vectors_for_shares, ): """ When mutable share data with an expired lease is rewritten using @@ -1013,8 +986,7 @@ class ShareTests(TestCase): secrets=secrets, tw_vectors={ k: v.for_call() - for (k, v) - in test_and_write_vectors_for_shares.items() + for (k, v) in test_and_write_vectors_for_shares.items() }, r_vector=[], ) @@ -1038,17 +1010,23 @@ class ShareTests(TestCase): # marked as expiring one additional lease period into the future. self.assertThat( self.server.remote_stat_shares([storage_index]), - Equals([{ - num: ShareStat( - size=get_implied_data_length( - test_and_write_vectors_for_shares[num].write_vector, - test_and_write_vectors_for_shares[num].new_length, - ), - lease_expiration=int(self.clock.seconds() + self.server.LEASE_PERIOD.total_seconds()), - ) - for num - in test_and_write_vectors_for_shares - }]), + Equals( + [ + { + num: ShareStat( + size=get_implied_data_length( + test_and_write_vectors_for_shares[num].write_vector, + test_and_write_vectors_for_shares[num].new_length, + ), + lease_expiration=int( + self.clock.seconds() + + self.server.LEASE_PERIOD.total_seconds() + ), + ) + for num in test_and_write_vectors_for_shares + } + ] + ), ) @given( @@ -1060,7 +1038,9 @@ class ShareTests(TestCase): ), test_and_write_vectors_for_shares=slot_test_and_write_vectors_for_shares(), ) - def test_client_cannot_control_lease_behavior(self, storage_index, secrets, test_and_write_vectors_for_shares): + def test_client_cannot_control_lease_behavior( + self, storage_index, secrets, test_and_write_vectors_for_shares + ): """ If the client passes ``renew_leases`` to *slot_testv_and_readv_and_writev* it fails with ``TypeError``, no lease is updated, and no share data is @@ -1087,11 +1067,7 @@ class ShareTests(TestCase): # secrets secrets, # tw_vectors - { - k: v.for_call() - for (k, v) - in test_and_write_vectors_for_shares.items() - }, + {k: v.for_call() for (k, v) in test_and_write_vectors_for_shares.items()}, # r_vector [], # add_leases @@ -1116,8 +1092,7 @@ class ShareTests(TestCase): shares=None, r_vector=list( list(map(write_vector_to_read_vector, vector.write_vector)) - for vector - in test_and_write_vectors_for_shares.values() + for vector in test_and_write_vectors_for_shares.values() ), ) self.expectThat( @@ -1134,7 +1109,9 @@ class ShareTests(TestCase): ) -def assert_read_back_data(self, storage_index, secrets, test_and_write_vectors_for_shares): +def assert_read_back_data( + self, storage_index, secrets, test_and_write_vectors_for_shares +): """ Assert that the data written by ``test_and_write_vectors_for_shares`` can be read back from ``storage_index``. @@ -1150,22 +1127,17 @@ def assert_read_back_data(self, storage_index, secrets, test_and_write_vectors_f # Create a buffer and pile up all the write operations in it. # This lets us make correct assertions about overlapping writes. for sharenum, vectors in test_and_write_vectors_for_shares.items(): - length = max( - offset + len(data) - for (offset, data) - in vectors.write_vector - ) + length = max(offset + len(data) for (offset, data) in vectors.write_vector) expected = b"\x00" * length for (offset, data) in vectors.write_vector: - expected = expected[:offset] + data + expected[offset + len(data):] + expected = expected[:offset] + data + expected[offset + len(data) :] if vectors.new_length is not None and vectors.new_length < length: - expected = expected[:vectors.new_length] + expected = expected[: vectors.new_length] expected_result = list( # Get the expected value out of our scratch buffer. - expected[offset:offset + len(data)] - for (offset, data) - in vectors.write_vector + expected[offset : offset + len(data)] + for (offset, data) in vectors.write_vector ) _, single_read = extract_result( diff --git a/src/_zkapauthorizer/tests/test_storage_server.py b/src/_zkapauthorizer/tests/test_storage_server.py index 1adab62a8b9e6e6605424f2a2ee78dc75f2f87bd..49a1701a3dc3ddfadbdb8f912549b978e851d5e7 100644 --- a/src/_zkapauthorizer/tests/test_storage_server.py +++ b/src/_zkapauthorizer/tests/test_storage_server.py @@ -16,94 +16,45 @@ Tests for ``_zkapauthorizer._storage_server``. """ -from __future__ import ( - absolute_import, - division, -) - -from time import ( - time, -) -from random import ( - shuffle, -) - -from testtools import ( - TestCase, -) -from testtools.matchers import ( - Equals, - AfterPreprocessing, - MatchesAll, -) -from hypothesis import ( - given, - note, -) -from hypothesis.strategies import ( - integers, - lists, - tuples, - one_of, - just, -) -from challenge_bypass_ristretto import ( - RandomToken, - random_signing_key, -) - -from twisted.python.runtime import ( - platform, -) -from twisted.internet.task import ( - Clock, -) - -from foolscap.referenceable import ( - LocalReferenceable, -) - -from .common import ( - skipIf, -) -from .privacypass import ( - make_passes, -) -from .matchers import ( - raises, -) -from .strategies import ( - zkaps, - sizes, - sharenum_sets, - storage_indexes, - write_enabler_secrets, - lease_renew_secrets, - lease_cancel_secrets, - slot_test_and_write_vectors_for_shares, -) -from .fixtures import ( - AnonymousStorageServer, -) -from .storage_common import ( - cleanup_storage_server, - write_toy_shares, -) -from ..api import ( - ZKAPAuthorizerStorageServer, - MorePassesRequired, -) +from __future__ import absolute_import, division + +from random import shuffle +from time import time + +from challenge_bypass_ristretto import RandomToken, random_signing_key +from foolscap.referenceable import LocalReferenceable +from hypothesis import given, note +from hypothesis.strategies import integers, just, lists, one_of, tuples +from testtools import TestCase +from testtools.matchers import AfterPreprocessing, Equals, MatchesAll +from twisted.internet.task import Clock +from twisted.python.runtime import platform + +from .._storage_server import _ValidationResult +from ..api import MorePassesRequired, ZKAPAuthorizerStorageServer from ..storage_common import ( - required_passes, - allocate_buckets_message, add_lease_message, - slot_testv_and_readv_and_writev_message, + allocate_buckets_message, get_implied_data_length, get_required_new_passes_for_mutable_write, + required_passes, + slot_testv_and_readv_and_writev_message, summarize, ) -from .._storage_server import ( - _ValidationResult, +from .common import skipIf +from .fixtures import AnonymousStorageServer +from .matchers import raises +from .privacypass import make_passes +from .storage_common import cleanup_storage_server, write_toy_shares +from .strategies import ( + lease_cancel_secrets, + lease_renew_secrets, + sharenum_sets, + sizes, + slot_test_and_write_vectors_for_shares, + storage_indexes, + write_enabler_secrets, + zkaps, ) @@ -111,6 +62,7 @@ class ValidationResultTests(TestCase): """ Tests for ``_ValidationResult``. """ + def setUp(self): super(ValidationResultTests, self).setUp() self.signing_key = random_signing_key() @@ -128,9 +80,7 @@ class ValidationResultTests(TestCase): list(RandomToken.create() for i in range(valid_count)), ) all_passes = valid_passes + list( - pass_.pass_text.encode("ascii") - for pass_ - in invalid_passes + pass_.pass_text.encode("ascii") for pass_ in invalid_passes ) shuffle(all_passes) @@ -144,14 +94,12 @@ class ValidationResultTests(TestCase): _ValidationResult( valid=list( idx - for (idx, pass_) - in enumerate(all_passes) + for (idx, pass_) in enumerate(all_passes) if pass_ in valid_passes ), signature_check_failed=list( idx - for (idx, pass_) - in enumerate(all_passes) + for (idx, pass_) in enumerate(all_passes) if pass_ not in valid_passes ), ), @@ -183,7 +131,9 @@ class ValidationResultTests(TestCase): ), AfterPreprocessing( str, - Equals("MorePassesRequired(valid_count=4, required_count=10, signature_check_failed=frozenset([4]))"), + Equals( + "MorePassesRequired(valid_count=4, required_count=10, signature_check_failed=frozenset([4]))" + ), ), ), ) @@ -193,6 +143,7 @@ class PassValidationTests(TestCase): """ Tests for pass validation performed by ``ZKAPAuthorizerStorageServer``. """ + pass_value = 128 * 1024 @skipIf(platform.isWindows(), "Storage server is not supported on Windows") @@ -238,13 +189,14 @@ class PassValidationTests(TestCase): allocate_buckets = lambda: self.storage_server.doRemoteCall( "allocate_buckets", - (valid_passes, - storage_index, - renew_secret, - cancel_secret, - share_nums, - allocated_size, - LocalReferenceable(None), + ( + valid_passes, + storage_index, + renew_secret, + cancel_secret, + share_nums, + allocated_size, + LocalReferenceable(None), ), {}, ) @@ -253,7 +205,6 @@ class PassValidationTests(TestCase): raises(MorePassesRequired), ) - @given( storage_index=storage_indexes(), secrets=tuples( @@ -305,13 +256,12 @@ class PassValidationTests(TestCase): else: self.fail("expected MorePassesRequired, got {}".format(result)) - def _test_extend_mutable_fails_without_passes( - self, - storage_index, - secrets, - test_and_write_vectors_for_shares, - make_data_vector, + self, + storage_index, + secrets, + test_and_write_vectors_for_shares, + make_data_vector, ): """ Verify that increasing the storage requirements of a slot without @@ -327,9 +277,7 @@ class PassValidationTests(TestCase): cleanup_storage_server(self.anonymous_storage_server) tw_vectors = { - k: v.for_call() - for (k, v) - in test_and_write_vectors_for_shares.items() + k: v.for_call() for (k, v) in test_and_write_vectors_for_shares.items() } note("tw_vectors summarized: {}".format(summarize(tw_vectors))) @@ -344,11 +292,7 @@ class PassValidationTests(TestCase): valid_passes = make_passes( self.signing_key, slot_testv_and_readv_and_writev_message(storage_index), - list( - RandomToken.create() - for i - in range(required_pass_count) - ), + list(RandomToken.create() for i in range(required_pass_count)), ) # Create an initial share to toy with. @@ -417,7 +361,9 @@ class PassValidationTests(TestCase): ), test_and_write_vectors_for_shares=slot_test_and_write_vectors_for_shares(), ) - def test_extend_mutable_with_write_fails_without_passes(self, storage_index, secrets, test_and_write_vectors_for_shares): + def test_extend_mutable_with_write_fails_without_passes( + self, storage_index, secrets, test_and_write_vectors_for_shares + ): """ If ``remote_slot_testv_and_readv_and_writev`` is invoked to increase storage usage by performing a write past the end of a share without @@ -435,13 +381,13 @@ class PassValidationTests(TestCase): ) def _test_lease_operation_fails_without_passes( - self, - storage_index, - secrets, - sharenums, - allocated_size, - lease_operation, - lease_operation_message, + self, + storage_index, + secrets, + sharenums, + allocated_size, + lease_operation, + lease_operation_message, ): """ Assert that a lease-taking operation fails if it is not supplied with @@ -461,7 +407,9 @@ class PassValidationTests(TestCase): renew_secret, cancel_secret = secrets - required_count = required_passes(self.pass_value, [allocated_size] * len(sharenums)) + required_count = required_passes( + self.pass_value, [allocated_size] * len(sharenums) + ) # Create some shares at a slot which will require lease renewal. write_toy_shares( self.anonymous_storage_server, @@ -514,16 +462,20 @@ class PassValidationTests(TestCase): sharenums=sharenum_sets(), allocated_size=sizes(), ) - def test_add_lease_fails_without_passes(self, storage_index, secrets, sharenums, allocated_size): + def test_add_lease_fails_without_passes( + self, storage_index, secrets, sharenums, allocated_size + ): """ If ``remote_add_lease`` is invoked without supplying enough passes to cover the storage for all shares on the given storage index, the operation fails with ``MorePassesRequired``. """ renew_secret, cancel_secret = secrets + def add_lease(storage_server, passes): return storage_server.doRemoteCall( - "add_lease", ( + "add_lease", + ( passes, storage_index, renew_secret, @@ -531,6 +483,7 @@ class PassValidationTests(TestCase): ), {}, ) + return self._test_lease_operation_fails_without_passes( storage_index, secrets, @@ -550,7 +503,9 @@ class PassValidationTests(TestCase): sharenums=one_of(just(None), sharenum_sets()), test_and_write_vectors_for_shares=slot_test_and_write_vectors_for_shares(), ) - def test_mutable_share_sizes(self, slot, secrets, sharenums, test_and_write_vectors_for_shares): + def test_mutable_share_sizes( + self, slot, secrets, sharenums, test_and_write_vectors_for_shares + ): """ ``share_sizes`` returns the size of the requested mutable shares in the requested slot. @@ -560,9 +515,7 @@ class PassValidationTests(TestCase): cleanup_storage_server(self.anonymous_storage_server) tw_vectors = { - k: v.for_call() - for (k, v) - in test_and_write_vectors_for_shares.items() + k: v.for_call() for (k, v) in test_and_write_vectors_for_shares.items() } # Create an initial share to toy with. @@ -574,11 +527,7 @@ class PassValidationTests(TestCase): valid_passes = make_passes( self.signing_key, slot_testv_and_readv_and_writev_message(slot), - list( - RandomToken.create() - for i - in range(required_pass_count) - ), + list(RandomToken.create() for i in range(required_pass_count)), ) test, read = self.storage_server.doRemoteCall( "slot_testv_and_readv_and_writev", @@ -599,13 +548,13 @@ class PassValidationTests(TestCase): expected_sizes = { sharenum: get_implied_data_length(data_vector, new_length) - for (sharenum, (testv, data_vector, new_length)) - in tw_vectors.items() + for (sharenum, (testv, data_vector, new_length)) in tw_vectors.items() if sharenums is None or sharenum in sharenums } actual_sizes = self.storage_server.doRemoteCall( - "share_sizes", ( + "share_sizes", + ( slot, sharenums, ), diff --git a/src/_zkapauthorizer/tests/test_strategies.py b/src/_zkapauthorizer/tests/test_strategies.py index 3f60fcd8a617e344e20b7c5e2bb496143e07d571..b046450cc8289561a141b1e1e62b3979d4638692 100644 --- a/src/_zkapauthorizer/tests/test_strategies.py +++ b/src/_zkapauthorizer/tests/test_strategies.py @@ -16,41 +16,22 @@ Tests for our custom Hypothesis strategies. """ -from __future__ import ( - absolute_import, -) +from __future__ import absolute_import -from testtools import ( - TestCase, -) +from allmydata.client import config_from_string +from fixtures import TempDir +from hypothesis import given, note +from hypothesis.strategies import data, just, one_of +from testtools import TestCase -from fixtures import ( - TempDir, -) +from .strategies import share_parameters, tahoe_config_texts -from hypothesis import ( - given, - note, -) -from hypothesis.strategies import ( - data, - one_of, - just, -) - -from allmydata.client import ( - config_from_string, -) - -from .strategies import ( - tahoe_config_texts, - share_parameters, -) class TahoeConfigsTests(TestCase): """ Tests for ``tahoe_configs``. """ + @given(data()) def test_parses(self, data): """ diff --git a/src/_zkapauthorizer/validators.py b/src/_zkapauthorizer/validators.py index bd1545144b3a9ed39d10c656ccd9ebbbde549804..0b7284a54cc2894dddc41c426c88b2a4e843ea10 100644 --- a/src/_zkapauthorizer/validators.py +++ b/src/_zkapauthorizer/validators.py @@ -16,9 +16,8 @@ This module implements validators for ``attrs``-defined attributes. """ -from base64 import ( - b64decode, -) +from base64 import b64decode + def is_base64_encoded(b64decode=b64decode): def validate_is_base64_encoded(inst, attr, value): @@ -31,8 +30,10 @@ def is_base64_encoded(b64decode=b64decode): value=value, ), ) + return validate_is_base64_encoded + def has_length(expected): def validate_has_length(inst, attr, value): if len(value) != expected: @@ -43,8 +44,10 @@ def has_length(expected): actual=len(value), ), ) + return validate_has_length + def greater_than(expected): def validate_relation(inst, attr, value): if value > expected: @@ -57,4 +60,5 @@ def greater_than(expected): actual=value, ), ) + return validate_relation diff --git a/src/twisted/plugins/zkapauthorizer.py b/src/twisted/plugins/zkapauthorizer.py index 0b02795d806ebcb58bf279eac9f744d4e539c360..85448fe604e6c787c906a878cacd4f6524d6fbc7 100644 --- a/src/twisted/plugins/zkapauthorizer.py +++ b/src/twisted/plugins/zkapauthorizer.py @@ -16,8 +16,6 @@ A drop-in to supply plugins to the Twisted plugin system. """ -from _zkapauthorizer.api import ( - ZKAPAuthorizer, -) +from _zkapauthorizer.api import ZKAPAuthorizer storage_server = ZKAPAuthorizer() diff --git a/tests.nix b/tests.nix index 961d29d3399ff0bec7f9e0af63fae3cef12a86c8..b8b5b39b0d656f43d07cd5c66938c7f52a28bdd2 100644 --- a/tests.nix +++ b/tests.nix @@ -32,6 +32,14 @@ in packagesExtra = [ zkapauthorizer ]; _.hypothesis.postUnpack = ""; }; + + lint-python = mach-nix.mkPython { + python = "python39"; + requirements = '' + isort + black + ''; + }; in pkgs.runCommand "zkapauthorizer-tests" { passthru = { @@ -42,6 +50,8 @@ in pushd ${zkapauthorizer.src} ${python}/bin/pyflakes + ${lint-python}/bin/black --check src + ${lint-python}/bin/isort --check src popd ZKAPAUTHORIZER_HYPOTHESIS_PROFILE=${hypothesisProfile'} ${python}/bin/python -m ${if collectCoverage