diff --git a/.circleci/config.yml b/.circleci/config.yml index 5a7a8f081f5207b8c318ccfcf836150fe934bfe7..bc94d6fd5440cf5a5ecee94c60329a3c64058437 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -71,14 +71,7 @@ jobs: - run: name: "Get Pip" command: | - # The CircleCI macOS environment has curl and Python but does not - # have pip. So, for starters, use curl and Python to get pip. - if [ "<< parameters.py-version >>" == "2.7" ]; then - - curl https://bootstrap.pypa.io/pip/2.7/get-pip.py -o get-pip.py - else - curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py - fi + curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py python<< parameters.py-version >> get-pip.py - run: @@ -136,6 +129,8 @@ jobs: linux-tests: &LINUX_TESTS parameters: + py-version: + type: "string" tahoe-lafs-source: # The name of a niv source in nix/sources.json which corresponds to # a Tahoe-LAFS version. This is the version that will be declared as a @@ -209,7 +204,8 @@ jobs: nix-build tests.nix \ --argstr hypothesisProfile ci \ --arg collectCoverage true \ - --argstr tahoe-lafs-source << parameters.tahoe-lafs-source >> + --argstr tahoe-lafs-source << parameters.tahoe-lafs-source >> \ + --argstr python python<< parameters.py-version >> - run: name: "Push to Cachix" @@ -241,18 +237,17 @@ workflows: jobs: - "documentation" - "linux-tests": - matrix: - parameters: - tahoe-lafs-source: - - "tahoe-lafs" + name: "Linux tests python 3.9" + py-version: "39" + tahoe-lafs-source: "tahoe-lafs" + + # https://circleci.com/docs/2.0/testing-ios/#supported-xcode-versions + - "macos-tests": + name: "macOS tests python 3.8 xcode 11.7.0" + py-version: "3.8" + xcode-version: "11.7.0" - "macos-tests": - matrix: - parameters: - py-version: - - "2.7" - - xcode-version: - # https://circleci.com/docs/2.0/testing-ios/#supported-xcode-versions - - "12.3.0" - - "11.7.0" + name: "macOS tests python 3.9 xcode 12.3.0" + py-version: "3.9" + xcode-version: "12.3.0" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8ad846a84580a397c4b451643c401b80e5d2e438..28c3479b5199d7326919b7c840019c279eff55d3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ jobs: strategy: matrix: python-version: - - "2.7" + - "3.9" steps: # Avoid letting Windows newlines confusing milksnake. diff --git a/default.nix b/default.nix index 85817ddefc528b87e8d90e42d9da04dc6a4b3f4b..47247993c937d7d3899c7667e9b22a3df8fe8f81 100644 --- a/default.nix +++ b/default.nix @@ -1,17 +1,19 @@ let sources = import nix/sources.nix; in -{ pkgs ? import sources.release2105 {} +{ pkgs ? import sources.release2111 { } , pypiData ? sources.pypi-deps-db -, mach-nix ? import sources.mach-nix { inherit pkgs pypiData; } +, python ? "python39" +, mach-nix ? import sources.mach-nix { inherit pkgs pypiData python; } , tahoe-lafs-source ? "tahoe-lafs" , tahoe-lafs-repo ? sources.${tahoe-lafs-source} +, ... }: let lib = pkgs.lib; - python = "python27"; providers = { _default = "sdist,nixpkgs,wheel"; + # mach-nix doesn't provide a good way to depend on mach-nix packages, # so we get it as a nixpkgs dependency from an overlay. See below for # details. @@ -34,6 +36,18 @@ in # The version of Klein we get doesn't need / can't have the patch that # comes from the nixpkgs derivation mach-nix picks up from 21.05. klein = "wheel"; + + # - has an undetected poetry dependency and when trying to work around + # this another way, dependencies have undetected dependencies, easier + # to just use the wheel. + collections-extended = "wheel"; + isort = "wheel"; + + # The sdists for these packages have a different source/directory layout + # than the github archive the nixpkgs derivations expects to operate on + # so the sdists provider fails to build them. + tomli = "wheel"; + hypothesis = "wheel"; }; in rec { @@ -52,12 +66,21 @@ in # going on and discover the real version specified by `src` below. version = "1.17.0.post999"; # See https://github.com/DavHau/mach-nix/issues/190 - requirementsExtra = '' + requirementsExtra = + '' + # See https://github.com/DavHau/mach-nix/issues/190 pyrsistent < 0.17 - foolscap == 0.13.1 configparser eliot - ''; + foolscap >= 21.7.0 + + # undetected cryptography build dependency + # https://github.com/DavHau/mach-nix/issues/305 + setuptools_rust + # undetected tomli build dependency + # probably same underlying cause as cryptography issue + flit_core + ''; postPatch = '' cat > src/allmydata/_version.py <<EOF # This _version.py is generated by nix. diff --git a/nix/sources.json b/nix/sources.json index fdde626e48c8f4c98f24532a65da6829345b6f6e..3107940ad6eb2e03ecd62262606c802987a8cdce 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -29,10 +29,10 @@ "homepage": "", "owner": "DavHau", "repo": "pypi-deps-db", - "rev": "96d01556b4597c022647acbf8c3b58d2a99bc963", - "sha256": "0s6ll2hi40gj6mp2zdg7w3dq17g381gnfkm390mqgp574lmbq6yw", + "rev": "856d67ab093a68425c3896ec2961cea3b95ae93f", + "sha256": "0a7avm4xvm454gxy4dq0fc19j3f9ik8gf3kjpsqhszfkamrn9y0p", "type": "tarball", - "url": "https://github.com/DavHau/pypi-deps-db/archive/96d01556b4597c022647acbf8c3b58d2a99bc963.tar.gz", + "url": "https://github.com/DavHau/pypi-deps-db/archive/856d67ab093a68425c3896ec2961cea3b95ae93f.tar.gz", "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz" }, "release2105": { @@ -41,6 +41,18 @@ "url": "https://releases.nixos.org/nixos/21.05/nixos-21.05.3740.ce7a1190a0f/nixexprs.tar.xz", "url_template": "https://releases.nixos.org/nixos/21.05/nixos-21.05.3740.ce7a1190a0f/nixexprs.tar.xz" }, + "release2111": { + "branch": "release-21.11", + "description": "Nix Packages collection", + "homepage": "", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d887ac7aee92e8fc54dde9060d60d927afae9d69", + "sha256": "1bpgfv45b1yvrgpwdgc4fm4a6sav198yd41bsrvlmm3jn2wi6qx5", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/d887ac7aee92e8fc54dde9060d60d927afae9d69.tar.gz", + "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz" + }, "tahoe-lafs": { "branch": "master", "description": "The Tahoe-LAFS decentralized secure filesystem.", diff --git a/setup.cfg b/setup.cfg index 90384fb4aca8c44dd6c2c867cd60c194bef1d267..768163ca8a71a5f9f8efc5f7020ad57bff10b28b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,8 +10,8 @@ keywords = tahoe-lafs, storage, privacy, cryptography license = Apache 2.0 classifiers = Framework :: Twisted - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.9 author = PrivateStorage.io, LLC maintainer = PrivateStorage.io, LLC home-page = https://privatestorage.io/ @@ -34,14 +34,18 @@ packages = install_requires = attrs zope.interface - eliot + eliot >= 1.11,<2 aniso8601 python-challenge-bypass-ristretto # The pip resolver sometimes finds treq's dependencies first and these are # incompatible with Tahoe-LAFS'. So duplicate them here (the ones that # have been observed to cause problems). Twisted[tls,conch] >= 19.10.0 - tahoe-lafs >=1.17,<1.18 + + # Tahoe has no stable Python API but we use its Python API so there's + # basically no wiggle room here. We still use a (really tiny) range + # because our Nix packaging provides a Tahoe-LAFS with a .postNNN version. + tahoe-lafs >=1.17.0,<1.17.1 treq pyutil prometheus-client @@ -55,3 +59,10 @@ install_requires = [options.extras_require] test = coverage; fixtures; testtools; hypothesis + +[flake8] +# Enforce all pyflakes constraints, and also prohibit tabs for indentation. +# Reference: +# https://flake8.pycqa.org/en/latest/user/error-codes.html +# https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes +select = F, W191 diff --git a/shell.nix b/shell.nix index 023fc1bdb969c66f6b0d8da9d0c3b638396b96f1..c7ed13786f4d7d394237bcf99009423c2b22c639 100644 --- a/shell.nix +++ b/shell.nix @@ -3,12 +3,34 @@ { ... }@args: let tests = import ./tests.nix args; - inherit (tests) pkgs; + inherit (tests) privatestorage lint-python; + inherit (privatestorage) pkgs mach-nix tahoe-lafs zkapauthorizer; + + python-env = mach-nix.mkPython { + inherit (zkapauthorizer.meta.mach-nix) python providers; + overridesPre = [ + ( + self: super: { + inherit tahoe-lafs; + } + ) + ]; + requirements = + '' + ${builtins.readFile ./requirements/test.in} + ${zkapauthorizer.requirements} + ''; + }; in pkgs.mkShell { + # Avoid leaving .pyc all over the source tree when manually triggering tests + # runs. + PYTHONDONTWRITEBYTECODE = "1"; + buildInputs = [ - tests.python - tests.lint-python - pkgs.niv + # Provide the linting tools for interactive usage. + lint-python + # Supply all of the runtime and testing dependencies. + python-env ]; } diff --git a/src/_zkapauthorizer/_base64.py b/src/_zkapauthorizer/_base64.py index 473cb41cdaee3d4174dffcf7cc1ade7e5b63ac10..604797ec854612beb771e3516a615c4fd9cc02e3 100644 --- a/src/_zkapauthorizer/_base64.py +++ b/src/_zkapauthorizer/_base64.py @@ -16,8 +16,6 @@ This module implements base64 encoding-related functionality. """ -from __future__ import absolute_import - from base64 import b64decode as _b64decode from binascii import Error from re import compile as _compile diff --git a/src/_zkapauthorizer/_json.py b/src/_zkapauthorizer/_json.py new file mode 100644 index 0000000000000000000000000000000000000000..cd148c9576d49e3059208116b049e96238783d9f --- /dev/null +++ b/src/_zkapauthorizer/_json.py @@ -0,0 +1,23 @@ +# Copyright 2022 PrivateStorage.io, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from json import dumps as _dumps +from typing import Any + + +def dumps_utf8(o: Any) -> bytes: + """ + Serialize an object to a UTF-8-encoded JSON byte string. + """ + return _dumps(o).encode("utf-8") diff --git a/src/_zkapauthorizer/_plugin.py b/src/_zkapauthorizer/_plugin.py index f6105e631f0f151ecf0677f9d26dc2ab912cb811..95a45a4388485c136e00087d29db949046e84271 100644 --- a/src/_zkapauthorizer/_plugin.py +++ b/src/_zkapauthorizer/_plugin.py @@ -17,21 +17,19 @@ The Twisted plugin that glues the Zero-Knowledge Access Pass system into Tahoe-LAFS. """ -from __future__ import absolute_import - import random from datetime import datetime from functools import partial +from typing import Callable, List from weakref import WeakValueDictionary -try: - from typing import Callable -except ImportError: - pass - import attr from allmydata.client import _Client -from allmydata.interfaces import IAnnounceableStorageServer, IFoolscapStoragePlugin +from allmydata.interfaces import ( + IAnnounceableStorageServer, + IFilesystemNode, + IFoolscapStoragePlugin, +) from allmydata.node import MissingConfigEntry from challenge_bypass_ristretto import PublicKey, SigningKey from eliot import start_action @@ -291,7 +289,7 @@ def _create_maintenance_service(reactor, node_config, client_node): get_now=get_now, ) last_run_path = FilePath( - node_config.get_private_path(b"last-lease-maintenance-run") + node_config.get_private_path(u"last-lease-maintenance-run") ) # Create the service to periodically run the lease maintenance operation. return lease_maintenance_service( @@ -303,13 +301,16 @@ def _create_maintenance_service(reactor, node_config, client_node): ) -def get_root_nodes(client_node, node_config): +def get_root_nodes(client_node, node_config) -> List[IFilesystemNode]: + """ + Get the configured starting points for lease maintenance traversal. + """ try: - rootcap = node_config.get_private_config(b"rootcap") + rootcap = node_config.get_private_config("rootcap") except MissingConfigEntry: return [] else: - return [client_node.create_node_from_uri(rootcap)] + return [client_node.create_node_from_uri(rootcap.encode("utf-8"))] def load_signing_key(path): diff --git a/src/_zkapauthorizer/_stack.py b/src/_zkapauthorizer/_stack.py index c6d553371b9e064583f2cc28bcc976236b5c9a51..abb725dcc3430174c7b3d6df708f02ad23bafee5 100644 --- a/src/_zkapauthorizer/_stack.py +++ b/src/_zkapauthorizer/_stack.py @@ -36,7 +36,12 @@ def less_limited_stack(): More precisely, the soft stack limit is raised to the hard limit. """ soft, hard = getrlimit(RLIMIT_STACK) - # We can raise the soft limit to the hard limit and no higher. - setrlimit(RLIMIT_STACK, (hard, hard)) - yield - setrlimit(RLIMIT_STACK, (soft, hard)) + try: + # We can raise the soft limit to the hard limit and no higher. + setrlimit(RLIMIT_STACK, (hard, hard)) + except ValueError: + # Well, not on macOS: https://bugs.python.org/issue34602 + yield + else: + yield + setrlimit(RLIMIT_STACK, (soft, hard)) diff --git a/src/_zkapauthorizer/_storage_client.py b/src/_zkapauthorizer/_storage_client.py index 5f4e5685c896fcd4ed7bbc262b5c07780179529b..e8fe8c468507c60ea0fad0672798605c4f8ce62b 100644 --- a/src/_zkapauthorizer/_storage_client.py +++ b/src/_zkapauthorizer/_storage_client.py @@ -20,16 +20,15 @@ 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 functools import partial, wraps +from typing import Any, Dict, List, Optional, Tuple import attr from allmydata.interfaces import IStorageServer from allmydata.util.eliotutil import log_call_deferred from attr.validators import provides from eliot.twisted import inline_callbacks -from twisted.internet.defer import returnValue +from twisted.internet.defer import Deferred, returnValue from twisted.internet.interfaces import IReactorTime from twisted.python.reflect import namedAny from zope.interface import implementer @@ -46,6 +45,21 @@ from .storage_common import ( slot_testv_and_readv_and_writev_message, ) +Secrets = Tuple[bytes, bytes, bytes] +TestWriteVectors = Dict[ + int, + Tuple[ + List[ + Tuple[int, int, bytes, bytes], + ], + List[ + Tuple[int, bytes], + ], + Optional[int], + ], +] +ReadVector = List[Tuple[int, int]] + class IncorrectStorageServerReference(Exception): """ @@ -341,7 +355,7 @@ class ZKAPAuthorizerStorageClient(object): num_passes, partial( self._get_passes, - allocate_buckets_message(storage_index).encode("utf-8"), + allocate_buckets_message(storage_index), ), partial(self._spend_for_allocate_buckets, allocated_size), ) @@ -384,7 +398,7 @@ class ZKAPAuthorizerStorageClient(object): cancel_secret, ), num_passes, - partial(self._get_passes, add_lease_message(storage_index).encode("utf-8")), + partial(self._get_passes, add_lease_message(storage_index)), ) returnValue(result) @@ -417,12 +431,12 @@ class ZKAPAuthorizerStorageClient(object): @with_rref def slot_testv_and_readv_and_writev( self, - rref, - storage_index, - secrets, - tw_vectors, - r_vector, - ): + rref: Any, + storage_index: bytes, + secrets: Secrets, + tw_vectors: TestWriteVectors, + r_vector: ReadVector, + ) -> Deferred: # Read operations are free. num_passes = 0 @@ -431,7 +445,7 @@ class ZKAPAuthorizerStorageClient(object): tw_vectors = { sharenum: ( [ - (offset, length, "eq", specimen) + (offset, length, b"eq", specimen) for (offset, length, specimen) in test_vector ], data_vectors, @@ -489,7 +503,7 @@ class ZKAPAuthorizerStorageClient(object): num_passes, partial( self._get_passes, - slot_testv_and_readv_and_writev_message(storage_index).encode("utf-8"), + slot_testv_and_readv_and_writev_message(storage_index), ), ) returnValue(result) diff --git a/src/_zkapauthorizer/_storage_server.py b/src/_zkapauthorizer/_storage_server.py index 809b385118b2a2f27c32e623c5d97bea2b8fc3be..aa11097ae705904c287babd26905ab05e6b52bf1 100644 --- a/src/_zkapauthorizer/_storage_server.py +++ b/src/_zkapauthorizer/_storage_server.py @@ -21,14 +21,13 @@ 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 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 typing import Dict, List, Optional import attr from allmydata.interfaces import RIStorageServer, TestAndWriteVectorsForShares @@ -69,24 +68,6 @@ from .storage_common import ( slot_testv_and_readv_and_writev_message, ) -try: - from typing import Dict, List, Optional -except ImportError: - pass - -# The last Python 2-supporting prometheus_client nevertheless tries to use -# FileNotFoundError, an exception type from Python 3. Since that release, -# prometheus_client has dropped Python 2 support entirely so there is little -# hope of ever having this fixed upstream. When ZKAPAuthorizer is ported to -# Python 3, this should no longer be necessary. -def _prometheus_client_fix(): - import prometheus_client.exposition - - prometheus_client.exposition.FileNotFoundError = IOError - - -_prometheus_client_fix() - # See allmydata/storage/mutable.py SLOT_HEADER_SIZE = 468 LEASE_TRAILER_SIZE = 4 @@ -122,13 +103,13 @@ class _ValidationResult(object): """ Cryptographically check the validity of a single pass. - :param unicode message: The shared message for pass validation. + :param bytes message: The shared message for pass validation. :param Pass pass_: The pass to validate. :return bool: ``False`` (invalid) if the pass includes a valid signature, ``True`` (valid) otherwise. """ - assert isinstance(message, unicode), "message %r not unicode" % (message,) + assert isinstance(message, bytes), "message %r not bytes" % (message,) assert isinstance(pass_, Pass), "pass %r not a Pass" % (pass_,) try: preimage = TokenPreimage.decode_base64(pass_.preimage) @@ -136,7 +117,8 @@ class _ValidationResult(object): 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") + proposed_signature, + message, ) return invalid_pass except Exception: @@ -148,7 +130,7 @@ class _ValidationResult(object): """ Check all of the given passes for validity. - :param unicode message: The shared message for pass validation. + :param bytes message: The shared message for pass validation. :param list[bytes] passes: The encoded passes to validate. :param SigningKey signing_key: The signing key to use to check the passes. @@ -398,7 +380,7 @@ 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", + action_type="zkapauthorizer:storage-server:remote:share-sizes", storage_index_or_slot=storage_index_or_slot, ): return dict( @@ -443,7 +425,7 @@ class ZKAPAuthorizerStorageServer(Referenceable): Note that the lease is *not* renewed in this case (see #254). """ with start_action( - action_type=u"zkapauthorizer:storage-server:remote:slot-testv-and-readv-and-writev", + action_type="zkapauthorizer:storage-server:remote:slot-testv-and-readv-and-writev", storage_index=b2a(storage_index), path=storage_index_to_dir(storage_index), ): @@ -877,7 +859,7 @@ def get_share_path(storage_server, storage_index, sharenum): return ( FilePath(storage_server.sharedir) .preauthChild(storage_index_to_dir(storage_index)) - .child(u"{}".format(sharenum)) + .child("{}".format(sharenum)) ) diff --git a/src/_zkapauthorizer/config.py b/src/_zkapauthorizer/config.py index b30b43fbf7e3b49091ba8bc8fd4175000ee755fa..f13e895f8b6da3055d53ed1e37d62a4da43fc4f2 100644 --- a/src/_zkapauthorizer/config.py +++ b/src/_zkapauthorizer/config.py @@ -17,11 +17,7 @@ Helpers for reading values from the Tahoe-LAFS configuration. """ from datetime import timedelta - -try: - from typing import Optional -except ImportError: - pass +from typing import Optional from allmydata.node import _Config diff --git a/src/_zkapauthorizer/configutil.py b/src/_zkapauthorizer/configutil.py index 2df26fc6d8257e950f7ea29d2a51d61ad31ea16f..b9b167ec580852d2ca2676fe86784f3c5928b31b 100644 --- a/src/_zkapauthorizer/configutil.py +++ b/src/_zkapauthorizer/configutil.py @@ -16,8 +16,6 @@ Basic utilities related to the Tahoe configuration file. """ -from __future__ import absolute_import, division, print_function, unicode_literals - def _merge_dictionaries(dictionaries): """ diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py index ab52c5bc98a8eec0d253b364945effe31f396ce0..f58192bbe09d3c9e8d86e594aa83c8741f474ba4 100644 --- a/src/_zkapauthorizer/controller.py +++ b/src/_zkapauthorizer/controller.py @@ -17,15 +17,13 @@ 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 base64 import b64decode, b64encode from datetime import timedelta from functools import partial from hashlib import sha256 -from json import dumps, loads +from json import loads from operator import delitem, setitem -from sys import exc_info +from typing import List import attr import challenge_bypass_ristretto @@ -40,6 +38,7 @@ from twisted.web.client import Agent from zope.interface import Interface, implementer from ._base64 import urlsafe_b64decode +from ._json import dumps_utf8 from ._stack import less_limited_stack from .model import Error as model_Error from .model import Pass @@ -53,14 +52,18 @@ from .model import Voucher RETRY_INTERVAL = timedelta(milliseconds=1000) -@attr.s +# It would be nice to have frozen exception types but Failure.cleanFailure +# interacts poorly with these. +# https://twistedmatrix.com/trac/ticket/9641 +# https://twistedmatrix.com/trac/ticket/9771 +@attr.s(auto_attribs=True) class UnexpectedResponse(Exception): """ The issuer responded in an unexpected and unhandled way. """ - code = attr.ib() - body = attr.ib() + code: int = attr.ib() + body: bytes = attr.ib() class AlreadySpent(Exception): @@ -78,7 +81,7 @@ class Unpaid(Exception): """ -@attr.s(frozen=True) +@attr.s(auto_attribs=True) class UnrecognizedFailureReason(Exception): """ An attempt was made to redeem a voucher and the response contained an unknown reason. @@ -86,7 +89,7 @@ class UnrecognizedFailureReason(Exception): The redemption attempt may be automatically retried at some point. """ - response = attr.ib() + response: dict = attr.ib() @attr.s @@ -94,15 +97,18 @@ class RedemptionResult(object): """ Contain the results of an attempt to redeem a voucher for ZKAP material. - :ivar list[UnblindedToken] unblinded_tokens: The tokens which resulted - from the redemption. + :ivar unblinded_tokens: The tokens which resulted from the redemption. - :ivar unicode public_key: The public key which the server proved was - involved in the redemption process. + :ivar public_key: The public key which the server proved was involved in + the redemption process. """ - unblinded_tokens = attr.ib() - public_key = attr.ib() + unblinded_tokens: List[UnblindedToken] = attr.ib( + validator=attr.validators.instance_of(list), + ) + public_key: str = attr.ib( + validator=attr.validators.instance_of(str), + ) class IRedeemer(Interface): @@ -238,14 +244,14 @@ class ErrorRedeemer(object): configured error. """ - details = attr.ib(validator=attr.validators.instance_of(unicode)) + details = attr.ib(validator=attr.validators.instance_of(str)) @classmethod def make(cls, section_name, node_config, announcement, reactor): details = node_config.get_config( section=section_name, - option=u"details", - ).decode("ascii") + option="details", + ) return cls(details) def random_tokens_for_voucher(self, voucher, counter, count): @@ -325,7 +331,7 @@ def dummy_random_tokens(voucher, counter, count): # Padding is 96 (random token length) - 32 (decoded voucher # length) - 4 (fixed-width counter) b64encode( - v + u"{:0>4}{:0>60}".format(counter, n).encode("ascii"), + v + "{:0>4}{:0>60}".format(counter, n).encode("ascii"), ), ) @@ -340,7 +346,7 @@ class DummyRedeemer(object): really redeeming them, it makes up some fake ZKAPs and pretends those are the result. - :ivar unicode _public_key: The base64-encoded public key to return with + :ivar str _public_key: The base64-encoded public key to return with all successful redemption results. As with the tokens returned by this redeemer, chances are this is not actually a valid public key. Its corresponding private key certainly has not been used to sign @@ -348,7 +354,7 @@ class DummyRedeemer(object): """ _public_key = attr.ib( - validator=attr.validators.instance_of(unicode), + validator=attr.validators.instance_of(str), ) @classmethod @@ -356,8 +362,8 @@ class DummyRedeemer(object): return cls( node_config.get_config( section=section_name, - option=u"issuer-public-key", - ).decode(u"utf-8"), + option="issuer-public-key", + ), ) def random_tokens_for_voucher(self, voucher, counter, count): @@ -461,8 +467,8 @@ class RistrettoRedeemer(object): def make(cls, section_name, node_config, announcement, reactor): configured_issuer = node_config.get_config( section=section_name, - option=u"ristretto-issuer-root-url", - ).decode("ascii") + option="ristretto-issuer-root-url", + ) if announcement is not None: # Don't let us talk to a storage server that has a different idea # about who issues ZKAPs. We should lift this limitation (that is, we @@ -473,7 +479,7 @@ class RistrettoRedeemer(object): # If we aren't given an announcement then we're not being used in # the context of a specific storage server so the check is # unnecessary and impossible. - announced_issuer = announcement[u"ristretto-issuer-root-url"] + announced_issuer = announcement["ristretto-issuer-root-url"] if announced_issuer != configured_issuer: raise IssuerConfigurationMismatch(announced_issuer, configured_issuer) @@ -498,13 +504,14 @@ class RistrettoRedeemer(object): ) 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( + self._api_root.child("v1", "redeem").to_text(), + dumps_utf8( { - u"redeemVoucher": voucher.number.decode("ascii"), - u"redeemCounter": counter, - u"redeemTokens": list( - token.encode_base64() for token in blinded_tokens + "redeemVoucher": voucher.number.decode("ascii"), + "redeemCounter": counter, + "redeemTokens": list( + token.encode_base64().decode("ascii") + for token in blinded_tokens ), } ), @@ -517,26 +524,26 @@ class RistrettoRedeemer(object): except ValueError: raise UnexpectedResponse(response.code, response_body) - success = result.get(u"success", False) + success = result.get("success", False) if not success: - reason = result.get(u"reason", None) - if reason == u"double-spend": + reason = result.get("reason", None) + if reason == "double-spend": raise AlreadySpent(voucher) - elif reason == u"unpaid": + elif reason == "unpaid": raise Unpaid(voucher) raise UnrecognizedFailureReason(result) self._log.info( "Redeemed: {public_key} {proof} {count}", - public_key=result[u"public-key"], - proof=result[u"proof"], - count=len(result[u"signatures"]), + public_key=result["public-key"], + proof=result["proof"], + count=len(result["signatures"]), ) - marshaled_signed_tokens = result[u"signatures"] - marshaled_proof = result[u"proof"] - marshaled_public_key = result[u"public-key"] + marshaled_signed_tokens = result["signatures"] + marshaled_proof = result["proof"] + marshaled_public_key = result["public-key"] public_key = challenge_bypass_ristretto.PublicKey.decode_base64( marshaled_public_key.encode("ascii"), @@ -660,17 +667,17 @@ class PaymentController(object): redeeming a voucher, if no other count is given when the redemption is started. - :ivar set[unicode] allowed_public_keys: The base64-encoded public keys for + :ivar set[str] allowed_public_keys: The base64-encoded public keys for which to accept tokens. - :ivar dict[unicode, Redeeming] _active: A mapping from voucher identifiers + :ivar dict[str, Redeeming] _active: A mapping from voucher identifiers which currently have redemption attempts in progress to a ``Redeeming`` state representing the attempt. - :ivar dict[unicode, datetime] _error: A mapping from voucher identifiers + :ivar dict[str, datetime] _error: A mapping from voucher identifiers which have recently failed with an unrecognized, transient error. - :ivar dict[unicode, datetime] _unpaid: A mapping from voucher identifiers + :ivar dict[str, datetime] _unpaid: A mapping from voucher identifiers which have recently failed a redemption attempt due to an unpaid response from the redemption server to timestamps when the failure was observed. @@ -732,7 +739,7 @@ class PaymentController(object): ) def _retry_redemption(self): - for voucher in self._error.keys() + self._unpaid.keys(): + for voucher in list(self._error.keys()) + list(self._unpaid.keys()): if voucher in self._active: continue if self.get_voucher(voucher).state.should_start_redemption(): @@ -938,7 +945,7 @@ class PaymentController(object): ) self._error[voucher] = model_Error( finished=self.store.now(), - details=reason.getErrorMessage().decode("utf-8", "replace"), + details=reason.getErrorMessage(), ) return False @@ -982,22 +989,22 @@ class PaymentController(object): def get_redeemer(plugin_name, node_config, announcement, reactor): - section_name = u"storageclient.plugins.{}".format(plugin_name) + section_name = "storageclient.plugins.{}".format(plugin_name) redeemer_kind = node_config.get_config( section=section_name, - option=u"redeemer", - default=u"ristretto", + option="redeemer", + default="ristretto", ) return _REDEEMERS[redeemer_kind](section_name, node_config, announcement, reactor) _REDEEMERS = { - u"non": NonRedeemer.make, - u"dummy": DummyRedeemer.make, - u"double-spend": DoubleSpendRedeemer.make, - u"unpaid": UnpaidRedeemer.make, - u"error": ErrorRedeemer.make, - u"ristretto": RistrettoRedeemer.make, + "non": NonRedeemer.make, + "dummy": DummyRedeemer.make, + "double-spend": DoubleSpendRedeemer.make, + "unpaid": UnpaidRedeemer.make, + "error": ErrorRedeemer.make, + "ristretto": RistrettoRedeemer.make, } @@ -1024,9 +1031,8 @@ def bracket(first, last, between): except GeneratorExit: raise except: - info = exc_info() yield last() - raise info[0], info[1], info[2] + raise else: yield last() returnValue(result) diff --git a/src/_zkapauthorizer/eliot.py b/src/_zkapauthorizer/eliot.py index 8f607d8a0e66e7605ff4d66cd5640d759bba3cdb..e9f4d49304300ee1be5f169f5e53ba97623b996e 100644 --- a/src/_zkapauthorizer/eliot.py +++ b/src/_zkapauthorizer/eliot.py @@ -16,91 +16,89 @@ Eliot field, message, and action definitions for ZKAPAuthorizer. """ -from __future__ import absolute_import - from eliot import ActionType, Field, MessageType PRIVACYPASS_MESSAGE = Field( - u"message", - unicode, - u"The PrivacyPass request-binding data associated with a pass.", + "message", + str, + "The PrivacyPass request-binding data associated with a pass.", ) INVALID_REASON = Field( - u"reason", - unicode, - u"The reason given by the server for rejecting a pass as invalid.", + "reason", + str, + "The reason given by the server for rejecting a pass as invalid.", ) PASS_COUNT = Field( - u"count", + "count", int, - u"A number of passes.", + "A number of passes.", ) GET_PASSES = MessageType( - u"zkapauthorizer:get-passes", + "zkapauthorizer:get-passes", [PRIVACYPASS_MESSAGE, PASS_COUNT], - u"An attempt to spend passes is beginning.", + "An attempt to spend passes is beginning.", ) SPENT_PASSES = MessageType( - u"zkapauthorizer:spent-passes", + "zkapauthorizer:spent-passes", [PASS_COUNT], - u"An attempt to spend passes has succeeded.", + "An attempt to spend passes has succeeded.", ) INVALID_PASSES = MessageType( - u"zkapauthorizer:invalid-passes", + "zkapauthorizer:invalid-passes", [INVALID_REASON, PASS_COUNT], - u"An attempt to spend passes has found some to be invalid.", + "An attempt to spend passes has found some to be invalid.", ) RESET_PASSES = MessageType( - u"zkapauthorizer:reset-passes", + "zkapauthorizer:reset-passes", [PASS_COUNT], - u"Some passes involved in a failed spending attempt have not definitely been spent and are being returned for future use.", + "Some passes involved in a failed spending attempt have not definitely been spent and are being returned for future use.", ) SIGNATURE_CHECK_FAILED = MessageType( - u"zkapauthorizer:storage-client:signature-check-failed", + "zkapauthorizer:storage-client:signature-check-failed", [PASS_COUNT], - u"Some passes the client tried to use were rejected for having invalid signatures.", + "Some passes the client tried to use were rejected for having invalid signatures.", ) CALL_WITH_PASSES = ActionType( - u"zkapauthorizer:storage-client:call-with-passes", + "zkapauthorizer:storage-client:call-with-passes", [PASS_COUNT], [], - u"A storage operation is being started which may spend some passes.", + "A storage operation is being started which may spend some passes.", ) CURRENT_SIZES = Field( - u"current_sizes", + "current_sizes", dict, - u"A dictionary mapping the numbers of existing shares to their existing sizes.", + "A dictionary mapping the numbers of existing shares to their existing sizes.", ) TW_VECTORS_SUMMARY = Field( - u"tw_vectors_summary", + "tw_vectors_summary", dict, - u"A dictionary mapping share numbers from tw_vectors to test and write vector summaries.", + "A dictionary mapping share numbers from tw_vectors to test and write vector summaries.", ) NEW_SIZES = Field( - u"new_sizes", + "new_sizes", dict, - u"A dictionary like that of CURRENT_SIZES but for the sizes computed for the shares after applying tw_vectors.", + "A dictionary like that of CURRENT_SIZES but for the sizes computed for the shares after applying tw_vectors.", ) NEW_PASSES = Field( - u"new_passes", + "new_passes", int, - u"The number of passes computed as being required for the change in size.", + "The number of passes computed as being required for the change in size.", ) MUTABLE_PASSES_REQUIRED = MessageType( - u"zkapauthorizer:storage:mutable-passes-required", + "zkapauthorizer:storage:mutable-passes-required", [CURRENT_SIZES, TW_VECTORS_SUMMARY, NEW_SIZES, NEW_PASSES], - u"Some number of passes has been computed as the cost of updating a mutable.", + "Some number of passes has been computed as the cost of updating a mutable.", ) diff --git a/src/_zkapauthorizer/foolscap.py b/src/_zkapauthorizer/foolscap.py index 20ba99fde74bfcf6f0a3f92281867887864c0d1c..5ff7ff92b6e9d0a96afcf78d255847fa37be0193 100644 --- a/src/_zkapauthorizer/foolscap.py +++ b/src/_zkapauthorizer/foolscap.py @@ -17,8 +17,6 @@ Definitions related to the Foolscap-based protocol used by ZKAPAuthorizer to communicate between storage clients and servers. """ -from __future__ import absolute_import - import attr from allmydata.interfaces import Offset, RIStorageServer, StorageIndex from foolscap.api import Any, Copyable, DictOf, ListOf, RemoteCopy @@ -90,7 +88,7 @@ def add_passes(schema): :return foolscap.remoteinterface.RemoteMethodSchema: A schema like ``schema`` but with one additional required argument. """ - return add_arguments(schema, [(b"passes", _PassList)]) + return add_arguments(schema, [("passes", _PassList)]) def add_arguments(schema, kwargs): diff --git a/src/_zkapauthorizer/lease_maintenance.py b/src/_zkapauthorizer/lease_maintenance.py index 49adebf5253260cbf214a0671d284cb698c061dd..ed7d9672eee51c5d9c151c2f0d77f36b6ca0b678 100644 --- a/src/_zkapauthorizer/lease_maintenance.py +++ b/src/_zkapauthorizer/lease_maintenance.py @@ -20,11 +20,7 @@ refresh leases on all shares reachable from a root. from datetime import datetime, timedelta from errno import ENOENT from functools import partial - -try: - from typing import Any, Dict -except ImportError: - pass +from typing import Any, Dict, Iterable import attr from allmydata.interfaces import IDirectoryNode, IFilesystemNode @@ -44,11 +40,6 @@ from .controller import bracket from .foolscap import ShareStat from .model import ILeaseMaintenanceObserver -try: - from typing import Iterable -except ImportError: - pass - SERVICE_NAME = u"lease maintenance service" @@ -395,7 +386,7 @@ def lease_maintenance_service( """ interval_mean = lease_maint_config.crawl_interval_mean interval_range = lease_maint_config.crawl_interval_range - halfrange = interval_range / 2 + halfrange = interval_range // 2 def sample_interval_distribution(): return timedelta( @@ -507,7 +498,7 @@ def write_time_to_path(path, when): :param datetime when: The datetime to write. """ - path.setContent(when.isoformat()) + path.setContent(when.isoformat().encode("utf-8")) def read_time_from_path(path): @@ -526,7 +517,7 @@ def read_time_from_path(path): return None raise else: - return parse_datetime(when) + return parse_datetime(when.decode("ascii")) def visit_storage_indexes_from_root(visitor, get_root_nodes): diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py index 7530d2b2613538fdbe26f5a3e05b6a1b8d35eacf..726be23690610c5681067e6f857f0c2a345bf30b 100644 --- a/src/_zkapauthorizer/model.py +++ b/src/_zkapauthorizer/model.py @@ -19,17 +19,18 @@ the storage plugin. from datetime import datetime from functools import wraps -from json import dumps, loads +from json import loads from sqlite3 import OperationalError from sqlite3 import connect as _connect import attr -from aniso8601 import parse_datetime as _parse_datetime +from aniso8601 import parse_datetime from twisted.logger import Logger from twisted.python.filepath import FilePath from zope.interface import Interface, implementer from ._base64 import urlsafe_b64decode +from ._json import dumps_utf8 from .schema import get_schema_upgrades, get_schema_version, run_schema_upgrades from .storage_common import ( get_configured_pass_value, @@ -39,18 +40,6 @@ from .storage_common import ( from .validators import greater_than, has_length, is_base64_encoded -def parse_datetime(s, **kw): - """ - Like ``aniso8601.parse_datetime`` but accept unicode as well. - """ - if isinstance(s, unicode): - s = s.encode("utf-8") - assert isinstance(s, bytes) - if "delimiter" in kw and isinstance(kw["delimiter"], unicode): - kw["delimiter"] = kw["delimiter"].encode("utf-8") - return _parse_datetime(s, **kw) - - class ILeaseMaintenanceObserver(Interface): """ An object which is interested in receiving events related to the progress @@ -86,7 +75,7 @@ class NotEnoughTokens(Exception): """ -CONFIG_DB_NAME = u"privatestorageio-zkapauthz-v1.sqlite3" +CONFIG_DB_NAME = "privatestorageio-zkapauthz-v1.sqlite3" def open_and_initialize(path, connect=None): @@ -107,10 +96,9 @@ def open_and_initialize(path, connect=None): except OSError as e: raise StoreOpenError(e) - dbfile = path.asBytesMode().path try: conn = connect( - dbfile, + path.path, isolation_level="IMMEDIATE", ) except OperationalError as e: @@ -380,7 +368,7 @@ class VoucherStore(object): Store some unblinded tokens, for example as part of a backup-restore process. - :param list[unicode] unblinded_tokens: The unblinded tokens to store. + :param list[str] unblinded_tokens: The unblinded tokens to store. :param int group_id: The unique identifier of the redemption group to which these tokens belong. @@ -398,7 +386,7 @@ class VoucherStore(object): tokens. This voucher will be marked as redeemed to indicate it has fulfilled its purpose and has no further use for us. - :param unicode public_key: The encoded public key for the private key + :param str public_key: The encoded public key for the private key which was used to sign these tokens. :param list[UnblindedToken] unblinded_tokens: The unblinded tokens to @@ -411,9 +399,9 @@ class VoucherStore(object): inserted tokens, ``False`` otherwise. """ if completed: - voucher_state = u"redeemed" + voucher_state = "redeemed" else: - voucher_state = u"pending" + voucher_state = "pending" if spendable: token_count_increase = len(unblinded_tokens) @@ -683,7 +671,7 @@ class VoucherStore(object): ) tokens = cursor.fetchall() return { - u"unblinded-tokens": list(token for (token,) in tokens), + "unblinded-tokens": list(token for (token,) in tokens), } def start_lease_maintenance(self): @@ -720,9 +708,9 @@ class VoucherStore(object): return None [(started, count, finished)] = activity return LeaseMaintenanceActivity( - parse_datetime(started, delimiter=u" "), + parse_datetime(started, delimiter=" "), count, - parse_datetime(finished, delimiter=u" "), + parse_datetime(finished, delimiter=" "), ) @@ -901,7 +889,7 @@ class RandomToken(object): def _counter_attribute(): return attr.ib( validator=attr.validators.and_( - attr.validators.instance_of((int, long)), + attr.validators.instance_of(int), greater_than(-1), ), ) @@ -923,8 +911,8 @@ class Pending(object): def to_json_v1(self): return { - u"name": u"pending", - u"counter": self.counter, + "name": "pending", + "counter": self.counter, } @@ -944,9 +932,9 @@ class Redeeming(object): def to_json_v1(self): return { - u"name": u"redeeming", - u"started": self.started.isoformat(), - u"counter": self.counter, + "name": "redeeming", + "started": self.started.isoformat(), + "counter": self.counter, } @@ -962,16 +950,16 @@ class Redeemed(object): """ finished = attr.ib(validator=attr.validators.instance_of(datetime)) - token_count = attr.ib(validator=attr.validators.instance_of((int, long))) + token_count = attr.ib(validator=attr.validators.instance_of(int)) def should_start_redemption(self): return False def to_json_v1(self): return { - u"name": u"redeemed", - u"finished": self.finished.isoformat(), - u"token-count": self.token_count, + "name": "redeemed", + "finished": self.finished.isoformat(), + "token-count": self.token_count, } @@ -984,8 +972,8 @@ class DoubleSpend(object): def to_json_v1(self): return { - u"name": u"double-spend", - u"finished": self.finished.isoformat(), + "name": "double-spend", + "finished": self.finished.isoformat(), } @@ -1004,8 +992,8 @@ class Unpaid(object): def to_json_v1(self): return { - u"name": u"unpaid", - u"finished": self.finished.isoformat(), + "name": "unpaid", + "finished": self.finished.isoformat(), } @@ -1018,16 +1006,16 @@ class Error(object): """ finished = attr.ib(validator=attr.validators.instance_of(datetime)) - details = attr.ib(validator=attr.validators.instance_of(unicode)) + details = attr.ib(validator=attr.validators.instance_of(str)) def should_start_redemption(self): return True def to_json_v1(self): return { - u"name": u"error", - u"finished": self.finished.isoformat(), - u"details": self.details, + "name": "error", + "finished": self.finished.isoformat(), + "details": self.details, } @@ -1061,7 +1049,7 @@ class Voucher(object): expected_tokens = attr.ib( validator=attr.validators.optional( attr.validators.and_( - attr.validators.instance_of((int, long)), + attr.validators.instance_of(int), greater_than(0), ), ), @@ -1089,15 +1077,15 @@ class Voucher(object): @classmethod def from_row(cls, row): def state_from_row(state, row): - if state == u"pending": + if state == "pending": return Pending(counter=row[3]) - if state == u"double-spend": + if state == "double-spend": return DoubleSpend( - parse_datetime(row[0], delimiter=u" "), + parse_datetime(row[0], delimiter=" "), ) - if state == u"redeemed": + if state == "redeemed": return Redeemed( - parse_datetime(row[0], delimiter=u" "), + parse_datetime(row[0], delimiter=" "), row[1], ) raise ValueError("Unknown voucher state {}".format(state)) @@ -1112,59 +1100,59 @@ class Voucher(object): # value represents a leap second. However, since we also use # Python to generate the data in the first place, it should never # represent a leap second... I hope. - created=parse_datetime(created, delimiter=u" "), + created=parse_datetime(created, delimiter=" "), state=state_from_row(state, row[4:]), ) @classmethod def from_json(cls, json): values = loads(json) - version = values.pop(u"version") + version = values.pop("version") return getattr(cls, "from_json_v{}".format(version))(values) @classmethod def from_json_v1(cls, values): - state_json = values[u"state"] - state_name = state_json[u"name"] - if state_name == u"pending": - state = Pending(counter=state_json[u"counter"]) - elif state_name == u"redeeming": + state_json = values["state"] + state_name = state_json["name"] + if state_name == "pending": + state = Pending(counter=state_json["counter"]) + elif state_name == "redeeming": state = Redeeming( - started=parse_datetime(state_json[u"started"]), - counter=state_json[u"counter"], + started=parse_datetime(state_json["started"]), + counter=state_json["counter"], ) - elif state_name == u"double-spend": + elif state_name == "double-spend": state = DoubleSpend( - finished=parse_datetime(state_json[u"finished"]), + finished=parse_datetime(state_json["finished"]), ) - elif state_name == u"redeemed": + elif state_name == "redeemed": state = Redeemed( - finished=parse_datetime(state_json[u"finished"]), - token_count=state_json[u"token-count"], + finished=parse_datetime(state_json["finished"]), + token_count=state_json["token-count"], ) - elif state_name == u"unpaid": + elif state_name == "unpaid": state = Unpaid( - finished=parse_datetime(state_json[u"finished"]), + finished=parse_datetime(state_json["finished"]), ) - elif state_name == u"error": + elif state_name == "error": state = Error( - finished=parse_datetime(state_json[u"finished"]), - details=state_json[u"details"], + finished=parse_datetime(state_json["finished"]), + details=state_json["details"], ) else: raise ValueError("Unrecognized state {!r}".format(state_json)) return cls( - number=values[u"number"].encode("ascii"), - expected_tokens=values[u"expected-tokens"], + number=values["number"].encode("ascii"), + expected_tokens=values["expected-tokens"], created=None - if values[u"created"] is None - else parse_datetime(values[u"created"]), + if values["created"] is None + else parse_datetime(values["created"]), state=state, ) def to_json(self): - return dumps(self.marshal()) + return dumps_utf8(self.marshal()) def marshal(self): return self.to_json_v1() @@ -1172,9 +1160,9 @@ class Voucher(object): def to_json_v1(self): state = self.state.to_json_v1() return { - u"number": self.number.decode("ascii"), - u"expected-tokens": self.expected_tokens, - u"created": None if self.created is None else self.created.isoformat(), - u"state": state, - u"version": 1, + "number": self.number.decode("ascii"), + "expected-tokens": self.expected_tokens, + "created": None if self.created is None else self.created.isoformat(), + "state": state, + "version": 1, } diff --git a/src/_zkapauthorizer/private.py b/src/_zkapauthorizer/private.py index 535c00203840ce36eaa3c4e0a9c13cf6d261dd02..7e2adb00c9a4c4ea1f0ff8fd9e4cb4c6eb86f6db 100644 --- a/src/_zkapauthorizer/private.py +++ b/src/_zkapauthorizer/private.py @@ -10,8 +10,6 @@ Support code for applying token-based HTTP authorization rules to a Twisted Web resource hierarchy. """ -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. # diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py index f5db0531f7d9563ddcee051475e0c9cf2d6ff1ba..2a37249287e27d0f1f1dd7ab4d1abd32d8ad6a16 100644 --- a/src/_zkapauthorizer/resource.py +++ b/src/_zkapauthorizer/resource.py @@ -22,8 +22,8 @@ In the future it should also allow users to read statistics about token usage. """ from itertools import islice -from json import dumps, load, loads -from sys import maxint +from json import load, loads +from sys import maxsize from twisted.logger import Logger from twisted.web.http import BAD_REQUEST @@ -33,6 +33,7 @@ from zope.interface import Attribute from . import __version__ as _zkapauthorizer_version from ._base64 import urlsafe_b64decode +from ._json import dumps_utf8 from .config import get_configured_lease_duration from .controller import PaymentController, get_redeemer from .pricecalculator import PriceCalculator @@ -65,18 +66,18 @@ def get_token_count( Retrieve the configured voucher value, in number of tokens, from the given configuration. - :param unicode plugin_name: The plugin name to use to choose a + :param str plugin_name: The plugin name to use to choose a configuration section. :param _Config node_config: See ``from_configuration``. :param int default: The value to return if none is configured. """ - section_name = u"storageclient.plugins.{}".format(plugin_name) + section_name = "storageclient.plugins.{}".format(plugin_name) return int( node_config.get_config( section=section_name, - option=u"default-token-count", + option="default-token-count", default=NUM_TOKENS, ) ) @@ -109,7 +110,7 @@ def from_configuration( :return IZKAPRoot: The root of the resource hierarchy presented by the client side of the plugin. """ - plugin_name = u"privatestorageio-zkapauthz-v1" + plugin_name = "privatestorageio-zkapauthz-v1" if redeemer is None: redeemer = get_redeemer( plugin_name, @@ -140,7 +141,7 @@ def from_configuration( ) root = create_private_tree( - lambda: node_config.get_private_config(b"api_auth_token"), + lambda: node_config.get_private_config("api_auth_token").encode("utf-8"), authorizationless_resource_tree( store, controller, @@ -219,7 +220,7 @@ class _CalculatePrice(Resource): Calculate the price in ZKAPs to store or continue storing files specified sizes. """ - if wrong_content_type(request, u"application/json"): + if wrong_content_type(request, "application/json"): return NOT_DONE_YET application_json(request) @@ -228,18 +229,18 @@ class _CalculatePrice(Resource): body_object = loads(payload) except ValueError: request.setResponseCode(BAD_REQUEST) - return dumps( + return dumps_utf8( { "error": "could not parse request body", } ) try: - version = body_object[u"version"] - sizes = body_object[u"sizes"] + version = body_object["version"] + sizes = body_object["sizes"] except (TypeError, KeyError): request.setResponseCode(BAD_REQUEST) - return dumps( + return dumps_utf8( { "error": "could not read `version` and `sizes` properties", } @@ -247,17 +248,17 @@ class _CalculatePrice(Resource): if version != 1: request.setResponseCode(BAD_REQUEST) - return dumps( + return dumps_utf8( { "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 + isinstance(size, int) and size >= 0 for size in sizes ): request.setResponseCode(BAD_REQUEST) - return dumps( + return dumps_utf8( { "error": "did not find required positive integer sizes list in request", } @@ -266,10 +267,10 @@ class _CalculatePrice(Resource): application_json(request) price = self._price_calculator.calculate(sizes) - return dumps( + return dumps_utf8( { - u"price": price, - u"period": self._lease_period, + "price": price, + "period": self._lease_period, } ) @@ -280,14 +281,14 @@ def wrong_content_type(request, required_type): :param request: The request object to check. - :param unicode required_type: The required content-type (eg - ``u"application/json"``). + :param str required_type: The required content-type (eg + ``"application/json"``). :return bool: ``True`` if the content-type is wrong and an error response has been generated. ``False`` otherwise. """ actual_type = request.requestHeaders.getRawHeaders( - u"content-type", + "content-type", [None], )[0] if actual_type != required_type: @@ -303,7 +304,7 @@ def application_json(request): :param twisted.web.iweb.IRequest request: The request to modify. """ - request.responseHeaders.setRawHeaders(u"content-type", [u"application/json"]) + request.responseHeaders.setRawHeaders("content-type", ["application/json"]) class _ProjectVersion(Resource): @@ -313,7 +314,7 @@ class _ProjectVersion(Resource): def render_GET(self, request): application_json(request) - return dumps( + return dumps_utf8( { "version": _zkapauthorizer_version, } @@ -339,24 +340,24 @@ class _UnblindedTokenCollection(Resource): """ application_json(request) state = self._store.backup() - unblinded_tokens = state[u"unblinded-tokens"] + unblinded_tokens = state["unblinded-tokens"] limit = request.args.get(b"limit", [None])[0] if limit is not None: - limit = min(maxint, int(limit)) + limit = min(maxsize, int(limit)) position = request.args.get(b"position", [b""])[0].decode("utf-8") - return dumps( + return dumps_utf8( { - u"total": len(unblinded_tokens), - u"spendable": self._store.count_unblinded_tokens(), - u"unblinded-tokens": list( + "total": len(unblinded_tokens), + "spendable": self._store.count_unblinded_tokens(), + "unblinded-tokens": list( islice( (token for token in unblinded_tokens if token > position), limit ) ), - u"lease-maintenance-spending": self._lease_maintenance_activity(), + "lease-maintenance-spending": self._lease_maintenance_activity(), } ) @@ -365,17 +366,17 @@ class _UnblindedTokenCollection(Resource): Store some unblinded tokens. """ application_json(request) - unblinded_tokens = load(request.content)[u"unblinded-tokens"] + unblinded_tokens = load(request.content)["unblinded-tokens"] self._store.insert_unblinded_tokens(unblinded_tokens, group_id=0) - return dumps({}) + return dumps_utf8({}) def _lease_maintenance_activity(self): activity = self._store.get_latest_lease_maintenance_activity() if activity is None: return activity return { - u"when": activity.finished.isoformat(), - u"count": activity.passes_required, + "when": activity.finished.isoformat(), + "count": activity.passes_required, } @@ -401,14 +402,14 @@ class _VoucherCollection(Resource): try: payload = loads(request.content.read()) except Exception: - return bad_request(u"json request body required").render(request) - if payload.keys() != [u"voucher"]: + return bad_request("json request body required").render(request) + if payload.keys() != {"voucher"}: return bad_request( - u"request object must have exactly one key: 'voucher'" + "request object must have exactly one key: 'voucher'" ).render(request) - voucher = payload[u"voucher"] + voucher = payload["voucher"] if not is_syntactic_voucher(voucher): - return bad_request(u"submitted voucher is syntactically invalid").render( + return bad_request("submitted voucher is syntactically invalid").render( request ) @@ -420,9 +421,9 @@ class _VoucherCollection(Resource): def render_GET(self, request): application_json(request) - return dumps( + return dumps_utf8( { - u"vouchers": list( + "vouchers": list( self._controller.incorporate_transient_state(voucher).marshal() for voucher in self._store.list() ), @@ -444,12 +445,12 @@ def is_syntactic_voucher(voucher): """ :param voucher: A candidate object to inspect. - :return bool: ``True`` if and only if ``voucher`` is a unicode string + :return bool: ``True`` if and only if ``voucher`` is a text string containing a syntactically valid voucher. This says **nothing** about the validity of the represented voucher itself. A ``True`` result - only means the unicode string can be **interpreted** as a voucher. + only means the string can be **interpreted** as a voucher. """ - if not isinstance(voucher, unicode): + if not isinstance(voucher, str): return False if len(voucher) != 44: # TODO. 44 is the length of 32 bytes base64 encoded. This model @@ -480,7 +481,7 @@ class VoucherView(Resource): return self._voucher.to_json() -def bad_request(reason=u"Bad Request"): +def bad_request(reason="Bad Request"): """ :return IResource: A resource which can be rendered to produce a **BAD REQUEST** response. diff --git a/src/_zkapauthorizer/schema.py b/src/_zkapauthorizer/schema.py index 9fe72b695f76f3c3d7bed9628499f45be4e98f8d..c25dfa9a95a0175cf98d2fcd3ba4dee1e66df730 100644 --- a/src/_zkapauthorizer/schema.py +++ b/src/_zkapauthorizer/schema.py @@ -11,9 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -from __future__ import unicode_literals - """ This module defines the database schema used by the model interface. """ diff --git a/src/_zkapauthorizer/server/spending.py b/src/_zkapauthorizer/server/spending.py index f93f1561bdaa70c041e04c44b0e42981a0dcc5c9..66fe819a3b768ac48842b7b649aec52b4f987a62 100644 --- a/src/_zkapauthorizer/server/spending.py +++ b/src/_zkapauthorizer/server/spending.py @@ -1,9 +1,4 @@ -from __future__ import absolute_import, division, print_function, unicode_literals - -try: - from typing import Any -except ImportError: - pass +from typing import Any import attr from challenge_bypass_ristretto import PublicKey diff --git a/src/_zkapauthorizer/spending.py b/src/_zkapauthorizer/spending.py index 19b96ecb1f597e0ddbe26e1840853c6af7b0ef64..f718ac0056d609af45a9f0c5488ca6abb93e3571 100644 --- a/src/_zkapauthorizer/spending.py +++ b/src/_zkapauthorizer/spending.py @@ -16,10 +16,15 @@ A module for logic controlling the manner in which ZKAPs are spent. """ +from __future__ import annotations + +from typing import Callable, List, Tuple + import attr from zope.interface import Attribute, Interface, implementer from .eliot import GET_PASSES, INVALID_PASSES, RESET_PASSES, SPENT_PASSES +from .model import Pass, UnblindedToken class IPassGroup(Interface): @@ -101,7 +106,7 @@ class PassGroup(object): """ Track the state of a group of passes intended as payment for an operation. - :ivar unicode _message: The request binding message for this group of + :ivar _message: The request binding message for this group of passes. :ivar IPassFactory _factory: The factory which created this pass group. @@ -109,19 +114,21 @@ class PassGroup(object): :ivar list[Pass] passes: The passes of which this group consists. """ - _message = attr.ib() - _factory = attr.ib() - _tokens = attr.ib() + _message: bytes = attr.ib(validator=attr.validators.instance_of(bytes)) + _factory: IPassFactory = attr.ib(validator=attr.validators.provides(IPassFactory)) + _tokens: List[Tuple[UnblindedToken, Pass]] = attr.ib( + validator=attr.validators.instance_of(list) + ) @property - def passes(self): + def passes(self) -> List[Pass]: return list(pass_ for (unblinded_token, pass_) in self._tokens) @property - def unblinded_tokens(self): + def unblinded_tokens(self) -> List[UnblindedToken]: return list(unblinded_token for (unblinded_token, pass_) in self._tokens) - def split(self, select_indices): + def split(self, select_indices: List[int]) -> (PassGroup, PassGroup): selected = [] unselected = [] for idx, t in enumerate(self._tokens): @@ -134,19 +141,19 @@ class PassGroup(object): attr.evolve(self, tokens=unselected), ) - def expand(self, by_amount): + def expand(self, by_amount: int) -> PassGroup: return attr.evolve( self, tokens=self._tokens + self._factory.get(self._message, by_amount)._tokens, ) - def mark_spent(self): + def mark_spent(self) -> None: self._factory._mark_spent(self.unblinded_tokens) - def mark_invalid(self, reason): + def mark_invalid(self, reason) -> None: self._factory._mark_invalid(reason, self.unblinded_tokens) - def reset(self): + def reset(self) -> None: self._factory._reset(self.unblinded_tokens) @@ -158,12 +165,12 @@ class SpendingController(object): attempts when necessary. """ - get_unblinded_tokens = attr.ib() - discard_unblinded_tokens = attr.ib() - invalidate_unblinded_tokens = attr.ib() - reset_unblinded_tokens = attr.ib() + get_unblinded_tokens: Callable[[int], List[UnblindedToken]] = attr.ib() + discard_unblinded_tokens: Callable[[List[UnblindedToken]], None] = attr.ib() + invalidate_unblinded_tokens: Callable[[List[UnblindedToken]], None] = attr.ib() + reset_unblinded_tokens: Callable[[List[UnblindedToken]], None] = attr.ib() - tokens_to_passes = attr.ib() + tokens_to_passes: Callable[[bytes, List[UnblindedToken]], List[Pass]] = attr.ib() @classmethod def for_store(cls, tokens_to_passes, store): @@ -179,10 +186,10 @@ class SpendingController(object): unblinded_tokens = self.get_unblinded_tokens(num_passes) passes = self.tokens_to_passes(message, unblinded_tokens) GET_PASSES.log( - message=message, + message=message.decode("utf-8"), count=num_passes, ) - return PassGroup(message, self, zip(unblinded_tokens, passes)) + return PassGroup(message, self, list(zip(unblinded_tokens, passes))) def _mark_spent(self, unblinded_tokens): SPENT_PASSES.log( diff --git a/src/_zkapauthorizer/storage_common.py b/src/_zkapauthorizer/storage_common.py index bbe326a292b502024eef0b77b4181977c4f2509d..db04c1f650eae7b35a95a5eec6c6896103d2e0d3 100644 --- a/src/_zkapauthorizer/storage_common.py +++ b/src/_zkapauthorizer/storage_common.py @@ -16,9 +16,8 @@ Functionality shared between the storage client and server. """ -from __future__ import division - from base64 import b64encode +from typing import Callable import attr from pyutil.mathutil import div_ceil @@ -27,7 +26,7 @@ from .eliot import MUTABLE_PASSES_REQUIRED from .validators import greater_than -@attr.s(frozen=True, str=True) +@attr.s(str=True) class MorePassesRequired(Exception): """ Storage operations fail with ``MorePassesRequired`` when they are not @@ -43,27 +42,27 @@ class MorePassesRequired(Exception): 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))) + valid_count = attr.ib(validator=attr.validators.instance_of(int)) + required_count = attr.ib(validator=attr.validators.instance_of(int)) signature_check_failed = attr.ib(converter=frozenset) -def _message_maker(label): +def _message_maker(label: str) -> Callable[[str], bytes]: def make_message(storage_index): - return u"{label} {storage_index}".format( + return "{label} {storage_index}".format( label=label, - storage_index=b64encode(storage_index), - ) + storage_index=b64encode(storage_index).decode("ascii"), + ).encode("ascii") 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") +allocate_buckets_message = _message_maker("allocate_buckets") +add_lease_message = _message_maker("add_lease") slot_testv_and_readv_and_writev_message = _message_maker( - u"slot_testv_and_readv_and_writev" + "slot_testv_and_readv_and_writev" ) # The number of bytes we're willing to store for a lease period for each pass @@ -80,8 +79,8 @@ def get_configured_shares_needed(node_config): """ return int( node_config.get_config( - section=u"client", - option=u"shares.needed", + section="client", + option="shares.needed", default=3, ) ) @@ -96,8 +95,8 @@ def get_configured_shares_total(node_config): """ return int( node_config.get_config( - section=u"client", - option=u"shares.total", + section="client", + option="shares.total", default=10, ) ) @@ -111,11 +110,11 @@ def get_configured_pass_value(node_config): value is read from the **pass-value** option of the ZKAPAuthorizer plugin client section. """ - section_name = u"storageclient.plugins.privatestorageio-zkapauthz-v1" + section_name = "storageclient.plugins.privatestorageio-zkapauthz-v1" return int( node_config.get_config( section=section_name, - option=u"pass-value", + option="pass-value", default=BYTES_PER_PASS, ) ) @@ -125,17 +124,20 @@ 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" + section_name = "storageclient.plugins.privatestorageio-zkapauthz-v1" return set( node_config.get_config( section=section_name, - option=u"allowed-public-keys", + option="allowed-public-keys", ) .strip() .split(",") ) +_dict_values = type(dict().values()) + + def required_passes(bytes_per_pass, share_sizes): """ Calculate the number of passes that are required to store shares of the @@ -148,9 +150,9 @@ def required_passes(bytes_per_pass, share_sizes): :return int: The number of passes required to cover the storage cost. """ - if not isinstance(share_sizes, list): + if not isinstance(share_sizes, (list, _dict_values)): raise TypeError( - "Share sizes must be a list of integers, got {!r} instead".format( + "Share sizes must be a list (or dict_values) of integers, got {!r} instead".format( share_sizes, ), ) @@ -301,7 +303,7 @@ def pass_value_attribute(): """ return attr.ib( validator=attr.validators.and_( - attr.validators.instance_of((int, long)), + attr.validators.instance_of(int), greater_than(0), ), ) diff --git a/src/_zkapauthorizer/tests/eliot.py b/src/_zkapauthorizer/tests/eliot.py deleted file mode 100644 index ba010cf2b51b709f19abfa18e1e0023fa1a38418..0000000000000000000000000000000000000000 --- a/src/_zkapauthorizer/tests/eliot.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright 2019 PrivateStorage.io, LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Eliot testing helpers. -""" - -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. -def validate_logging(assertion, *assertionArgs, **assertionKwargs): - def decorator(function): - @wraps(function) - def wrapper(self, *args, **kwargs): - skipped = False - - kwargs["logger"] = logger = MemoryLogger() - self.addCleanup(check_for_errors, logger) - # TestCase runs cleanups in reverse order, and we want this to - # run *before* tracebacks are checked: - if assertion is not None: - self.addCleanup( - lambda: skipped - or assertion(self, logger, *assertionArgs, **assertionKwargs) - ) - try: - return function(self, *args, **kwargs) - except SkipTest: - skipped = True - raise - - return wrapper - - return decorator - - -def capture_logging(assertion, *assertionArgs, **assertionKwargs): - """ - Capture and validate all logging that doesn't specify a L{Logger}. - - See L{validate_logging} for details on the rest of its behavior. - """ - - def decorator(function): - @validate_logging(assertion, *assertionArgs, **assertionKwargs) - @wraps(function) - def wrapper(self, *args, **kwargs): - logger = kwargs["logger"] - previous_logger = swap_logger(logger) - - def cleanup(): - swap_logger(previous_logger) - - self.addCleanup(cleanup) - return function(self, *args, **kwargs) - - return wrapper - - return decorator diff --git a/src/_zkapauthorizer/tests/fixtures.py b/src/_zkapauthorizer/tests/fixtures.py index e017eadd6b42f10f6f4255caf5c110d64cfca7de..54d27b96c42014149e21d9cdcb83a6376d4df7f4 100644 --- a/src/_zkapauthorizer/tests/fixtures.py +++ b/src/_zkapauthorizer/tests/fixtures.py @@ -16,8 +16,6 @@ Common fixtures to let the test suite focus on application logic. """ -from __future__ import absolute_import - from base64 import b64encode import attr @@ -49,9 +47,9 @@ class AnonymousStorageServer(Fixture): clock = attr.ib() def _setUp(self): - self.tempdir = FilePath(self.useFixture(TempDir()).join(b"storage")) + self.tempdir = FilePath(self.useFixture(TempDir()).join(u"storage")) self.storage_server = StorageServer( - self.tempdir.asBytesMode().path, + self.tempdir.path, b"x" * 20, clock=self.clock, ) @@ -75,7 +73,7 @@ class TemporaryVoucherStore(Fixture): def _setUp(self): self.tempdir = self.useFixture(TempDir()) - self.config = self.get_config(self.tempdir.join(b"node"), b"tub.port") + self.config = self.get_config(self.tempdir.join(u"node"), u"tub.port") self.store = VoucherStore.from_node_config( self.config, self.get_now, diff --git a/src/_zkapauthorizer/tests/foolscap.py b/src/_zkapauthorizer/tests/foolscap.py index 3a984bea163fd4c567812556f8229508c0cb8a2d..8ad3345c45c44634052bb680032c4f8c0c18ede1 100644 --- a/src/_zkapauthorizer/tests/foolscap.py +++ b/src/_zkapauthorizer/tests/foolscap.py @@ -16,8 +16,6 @@ Testing helpers related to Foolscap. """ -from __future__ import absolute_import - import attr from allmydata.interfaces import RIStorageServer from foolscap.api import Any, Copyable, Referenceable, RemoteInterface @@ -80,7 +78,7 @@ class LocalTracker(object): self.interfaceName = self.interface.__remote_name__ def getURL(self): - return b"pb://abcd@127.0.0.1:12345/efgh" + return "pb://abcd@127.0.0.1:12345/efgh" @attr.s @@ -121,7 +119,7 @@ class LocalRemote(object): schema = self._referenceable.getInterface()[methname] if self.check_args: schema.checkAllArgs(args, kwargs, inbound=True) - _check_copyables(list(args) + kwargs.values()) + _check_copyables(list(args) + list(kwargs.values())) result = self._referenceable.doRemoteCall( methname, args, diff --git a/src/_zkapauthorizer/tests/json.py b/src/_zkapauthorizer/tests/json.py index b8aa7c74548cf3f9a744dcf0cd44bc03c7afb689..729641281365d53e5d5958a14b63bf2ec25c9748 100644 --- a/src/_zkapauthorizer/tests/json.py +++ b/src/_zkapauthorizer/tests/json.py @@ -16,8 +16,6 @@ A better JSON module. """ -from __future__ import absolute_import - from json import loads as _loads diff --git a/src/_zkapauthorizer/tests/privacypass.py b/src/_zkapauthorizer/tests/privacypass.py index a2c29cc4b9daee16bfb86f1f311f1b1c4b45e4f6..fb4fcc76a35e5236fab3a18120e65aecb12f3096 100644 --- a/src/_zkapauthorizer/tests/privacypass.py +++ b/src/_zkapauthorizer/tests/privacypass.py @@ -16,8 +16,6 @@ Ristretto-flavored PrivacyPass helpers for the test suite. """ -from __future__ import absolute_import - from challenge_bypass_ristretto import BatchDLEQProof, PublicKey from ..model import Pass @@ -30,7 +28,7 @@ def make_passes(signing_key, for_message, random_tokens): :param challenge_bypass_ristretto.SigningKey signing_key: The key to use to sign the passes. - :param unicode for_message: The request-binding message with which to + :param bytes for_message: The request-binding message with which to associate the passes. :param list[challenge_bypass_ristretto.RandomToken] random_tokens: The @@ -62,7 +60,7 @@ def make_passes(signing_key, for_message, random_tokens): for unblinded_signature in unblinded_signatures ) message_signatures = list( - verification_key.sign_sha512(for_message.encode("utf-8")) + verification_key.sign_sha512(for_message) for verification_key in verification_keys ) passes = list( diff --git a/src/_zkapauthorizer/tests/storage_common.py b/src/_zkapauthorizer/tests/storage_common.py index db95a71c78308c472b7ab60f96cb09e50a4e9ac4..a168395ca073ba197ece412a39dbcbf346c2a3f6 100644 --- a/src/_zkapauthorizer/tests/storage_common.py +++ b/src/_zkapauthorizer/tests/storage_common.py @@ -20,9 +20,10 @@ from functools import partial from itertools import islice from os import SEEK_CUR from struct import pack +from typing import Callable, Dict, List, Set import attr -from challenge_bypass_ristretto import RandomToken +from challenge_bypass_ristretto import RandomToken, SigningKey from twisted.python.filepath import FilePath from zope.interface import implementer @@ -134,7 +135,7 @@ def whitebox_write_sparse_share(sharepath, version, size, leases, now): ) -def integer_passes(limit): +def integer_passes(limit: int) -> Callable[[bytes, int], List[int]]: """ :return: A function which can be used to get a number of passes. The function accepts a unicode request-binding message and an integer @@ -152,9 +153,11 @@ def integer_passes(limit): return get_passes -def get_passes(message, count, signing_key): +def get_passes( + message: bytes, count: int, signing_key: SigningKey +) -> List[RandomToken]: """ - :param unicode message: Request-binding message for PrivacyPass. + :param bytes message: Request-binding message for PrivacyPass. :param int count: The number of passes to get. @@ -163,6 +166,7 @@ def get_passes(message, count, signing_key): :return list[Pass]: ``count`` new random passes signed with the given key and bound to the given message. """ + assert isinstance(message, bytes) return make_passes( signing_key, message, @@ -200,33 +204,32 @@ class _PassFactory(object): """ A stateful pass issuer. - :ivar (unicode -> int -> [bytes]) _get_passes: A function for getting - passes. + :ivar _get_passes: A function for getting passes. - :ivar set[int] in_use: All of the passes given out without a confirmed + :ivar in_use: All of the passes given out without a confirmed terminal state. - :ivar dict[int, unicode] invalid: All of the passes given out and returned - using ``IPassGroup.invalid`` mapped to the reason given. + :ivar invalid: All of the passes given out and returned using + ``IPassGroup.invalid`` mapped to the reason given. - :ivar set[int] spent: All of the passes given out and returned via + :ivar spent: All of the passes given out and returned via ``IPassGroup.mark_spent``. - :ivar set[int] issued: All of the passes ever given out. + :ivar issued: All of the passes ever given out. - :ivar list[int] returned: A list of passes which were given out but then - returned via ``IPassGroup.reset``. + :ivar returned: A list of passes which were given out but then returned + via ``IPassGroup.reset``. """ - _get_passes = attr.ib() + _get_passes: Callable[[bytes, int], List[bytes]] = attr.ib() - returned = attr.ib(default=attr.Factory(list), init=False) - in_use = attr.ib(default=attr.Factory(set), init=False) - invalid = attr.ib(default=attr.Factory(dict), init=False) - spent = attr.ib(default=attr.Factory(set), init=False) - issued = attr.ib(default=attr.Factory(set), init=False) + returned: List[int] = attr.ib(default=attr.Factory(list), init=False) + in_use: Set[int] = attr.ib(default=attr.Factory(set), init=False) + invalid: Dict[int, str] = attr.ib(default=attr.Factory(dict), init=False) + spent: Set[int] = attr.ib(default=attr.Factory(set), init=False) + issued: Set[int] = attr.ib(default=attr.Factory(set), init=False) - def get(self, message, num_passes): + def get(self, message: bytes, num_passes: int) -> PassGroup: passes = [] if self.returned: passes.extend(self.returned[:num_passes]) @@ -235,7 +238,7 @@ class _PassFactory(object): passes.extend(self._get_passes(message, num_passes)) self.issued.update(passes) self.in_use.update(passes) - return PassGroup(message, self, zip(passes, passes)) + return PassGroup(message, self, list(zip(passes, passes))) def _clear(self): """ diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py index 61b401967aa131463f7e83f03ba03bceda85c430..d468030d111774a4090ad3e544952b4651895f56 100644 --- a/src/_zkapauthorizer/tests/strategies.py +++ b/src/_zkapauthorizer/tests/strategies.py @@ -18,7 +18,7 @@ Hypothesis strategies for property testing. from base64 import b64encode, urlsafe_b64encode from datetime import datetime, timedelta -from urllib import quote +from urllib.parse import quote import attr from allmydata.client import config_from_string @@ -142,7 +142,7 @@ def tahoe_config_texts(storage_client_plugins, shares): def merge_shares(shares, the_rest): for (k, v) in zip(("needed", "happy", "total"), shares): if v is not None: - the_rest["shares." + k] = u"{}".format(v) + the_rest["shares." + k] = f"{v}" return the_rest client_section = builds( @@ -151,7 +151,7 @@ def tahoe_config_texts(storage_client_plugins, shares): fixed_dictionaries( { "storage.plugins": just( - u",".join(storage_client_plugins.keys()), + ",".join(storage_client_plugins.keys()), ), }, ), @@ -186,8 +186,8 @@ def minimal_tahoe_configs(storage_client_plugins=None, shares=just((None, None, :param shares: See ``tahoe_config_texts``. - :return SearchStrategy[unicode]: A strategy that builds unicode strings - which are Tahoe-LAFS configuration file contents. + :return SearchStrategy[str]: A strategy that builds text strings which are + Tahoe-LAFS configuration file contents. """ if storage_client_plugins is None: storage_client_plugins = {} @@ -207,9 +207,9 @@ def node_nicknames(): alphabet=characters( blacklist_categories={ # Surrogates - u"Cs", + "Cs", # Unnamed and control characters - u"Cc", + "Cc", }, ), ) @@ -241,23 +241,23 @@ def server_configurations(signing_key_path): """ Build configuration values for the server-side plugin. - :param unicode signing_key_path: A value to insert for the + :param str signing_key_path: A value to insert for the **ristretto-signing-key-path** item. """ return one_of( fixed_dictionaries( { - u"pass-value": + "pass-value": # The configuration is ini so everything is always a byte string! - integers(min_value=1).map(bytes), + integers(min_value=1).map(lambda v: f"{v}".encode("ascii")), } ), 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, + "ristretto-issuer-root-url": "https://issuer.example.invalid/", + "ristretto-signing-key-path": signing_key_path.path, } ) or config, @@ -294,8 +294,8 @@ def zkapauthz_configuration( allowed_public_keys, ): config = { - u"default-token-count": u"32", - u"allowed-public-keys": u",".join(allowed_public_keys), + "default-token-count": "32", + "allowed-public-keys": ",".join(allowed_public_keys), } config.update(extra_configuration) return config @@ -314,8 +314,8 @@ def client_ristrettoredeemer_configurations(): return zkapauthz_configuration( just( { - u"ristretto-issuer-root-url": u"https://issuer.example.invalid/", - u"redeemer": u"ristretto", + "ristretto-issuer-root-url": "https://issuer.example.invalid/", + "redeemer": "ristretto", } ) ) @@ -351,10 +351,10 @@ def client_dummyredeemer_configurations( extra_config = lease_configs.map( lambda config: config.update( { - u"redeemer": u"dummy", + "redeemer": "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)), + "issuer-public-key": next(iter(allowed_keys)), } ) or config, @@ -382,7 +382,7 @@ def client_doublespendredeemer_configurations(default_token_counts=token_counts( return zkapauthz_configuration( just( { - u"redeemer": u"double-spend", + "redeemer": "double-spend", } ) ) @@ -395,7 +395,7 @@ def client_unpaidredeemer_configurations(): return zkapauthz_configuration( just( { - u"redeemer": u"unpaid", + "redeemer": "unpaid", } ) ) @@ -408,7 +408,7 @@ def client_nonredeemer_configurations(): return zkapauthz_configuration( just( { - u"redeemer": u"non", + "redeemer": "non", } ) ) @@ -421,8 +421,8 @@ def client_errorredeemer_configurations(details): return zkapauthz_configuration( just( { - u"redeemer": u"error", - u"details": details, + "redeemer": "error", + "details": details, } ) ) @@ -492,14 +492,14 @@ def direct_tahoe_configs( """ config_texts = minimal_tahoe_configs( { - u"privatestorageio-zkapauthz-v1": zkapauthz_v1_configuration, + "privatestorageio-zkapauthz-v1": zkapauthz_v1_configuration, }, shares, ) return config_texts.map( lambda config_text: config_from_string( - u"/dev/null/illegal", - u"", + "/dev/null/illegal", + "", config_text.encode("utf-8"), ), ) @@ -525,7 +525,7 @@ def tahoe_configs( def path_setter(config): def set_paths(basedir, portnumfile): - config._basedir = basedir.decode("ascii") + config._basedir = basedir config.portnum_fname = portnumfile return config @@ -728,7 +728,11 @@ def request_paths(): :see: ``requests`` """ - return lists(text().map(lambda x: quote(x.encode("utf-8"), safe=b""))) + + def quote_segment(seg): + return quote(seg, safe="").encode("utf-8") + + return lists(text().map(quote_segment)) def requests(paths=request_paths()): @@ -845,7 +849,7 @@ def bytes_for_share(sharenum, size): given share number """ if 0 <= sharenum <= 255: - return (unichr(sharenum) * size).encode("latin-1") + return (chr(sharenum) * size).encode("latin-1") raise ValueError("Sharenum must be between 0 and 255 inclusive.") @@ -949,7 +953,7 @@ def announcements(): """ return just( { - u"ristretto-issuer-root-url": u"https://issuer.example.invalid/", + "ristretto-issuer-root-url": "https://issuer.example.invalid/", } ) @@ -977,7 +981,7 @@ class _DirectoryNode(object): _storage_index = attr.ib() _children = attr.ib() - def list(self): + def list(self): # noqa: F811 return succeed(self._children) def get_storage_index(self): diff --git a/src/_zkapauthorizer/tests/test_base64.py b/src/_zkapauthorizer/tests/test_base64.py index d91bbacd065acc75295c5b0bc8b2d67d790bc2f3..df3111d280bcc6afe0454e349fe7d453cc7a5d22 100644 --- a/src/_zkapauthorizer/tests/test_base64.py +++ b/src/_zkapauthorizer/tests/test_base64.py @@ -16,8 +16,6 @@ Tests for ``_zkapauthorizer._base64``. """ -from __future__ import absolute_import - from base64 import urlsafe_b64encode from hypothesis import given diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py index 7e3e9abf1067d27a2c3a00b44bc94c1f47c9526a..c8af0774d635a99bec0801e6bfe0bf80e9807a5c 100644 --- a/src/_zkapauthorizer/tests/test_client_resource.py +++ b/src/_zkapauthorizer/tests/test_client_resource.py @@ -17,12 +17,10 @@ Tests for the web resource provided by the client part of the Tahoe-LAFS plugin. """ -from __future__ import absolute_import - from datetime import datetime from io import BytesIO -from json import dumps -from urllib import quote +from typing import Optional, Set +from urllib.parse import quote import attr from allmydata.client import config_from_string @@ -30,6 +28,7 @@ from aniso8601 import parse_datetime from fixtures import TempDir from hypothesis import given, note from hypothesis.strategies import ( + SearchStrategy, binary, builds, datetimes, @@ -73,6 +72,7 @@ from twisted.web.resource import IResource, getChildForRequest from .. import __version__ as zkapauthorizer_version from .._base64 import urlsafe_b64decode +from .._json import dumps_utf8 from ..configutil import config_string_from_sections from ..model import ( DoubleSpend, @@ -110,7 +110,7 @@ from .strategies import ( vouchers, ) -TRANSIENT_ERROR = u"something went wrong, who knows what" +TRANSIENT_ERROR = "something went wrong, who knows what" # Helper to work-around https://github.com/twisted/treq/issues/161 def uncooperator(started=True): @@ -137,17 +137,19 @@ def is_not_json(bytestring): def not_vouchers(): """ - Builds unicode strings which are not legal vouchers. + Builds byte strings which are not legal vouchers. """ return one_of( - text().filter( + text() + .filter( lambda t: (not is_urlsafe_base64(t)), - ), + ) + .map(lambda t: t.encode("utf-8")), 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"/" + lambda voucher: b"/" + voucher[1:], ), ) @@ -155,7 +157,7 @@ def not_vouchers(): def is_urlsafe_base64(text): """ - :param unicode text: A candidate unicode string to inspect. + :param str text: A candidate text string to inspect. :return bool: ``True`` if and only if ``text`` is urlsafe-base64 encoded """ @@ -174,18 +176,18 @@ def invalid_bodies(): # The wrong key but the right kind of value. fixed_dictionaries( { - u"some-key": vouchers(), + "some-key": vouchers().map(lambda v: v.decode("utf-8")), } - ).map(dumps), + ).map(dumps_utf8), # The right key but the wrong kind of value. fixed_dictionaries( { - u"voucher": one_of( + "voucher": one_of( integers(), - not_vouchers(), + not_vouchers().map(lambda v: v.decode("utf-8")), ), } - ).map(dumps), + ).map(dumps_utf8), # Not even JSON binary().filter(is_not_json), ) @@ -242,8 +244,8 @@ def authorized_request(api_auth_token, agent, method, uri, headers=None, data=No else: headers = Headers(headers) headers.setRawHeaders( - u"authorization", - [b"tahoe-lafs {}".format(api_auth_token)], + "authorization", + [b"tahoe-lafs " + api_auth_token], ) return agent.request( method, @@ -267,8 +269,8 @@ def get_config_with_api_token(tempdir, get_config, api_auth_token): :param bytes api_auth_token: The HTTP API authorization token to write to the node directory. """ - basedir = tempdir.join(b"tahoe") - config = get_config(basedir, b"tub.port") + basedir = tempdir.join(u"tahoe") + config = get_config(basedir, u"tub.port") add_api_token_to_config( basedir, config, @@ -282,9 +284,9 @@ def add_api_token_to_config(basedir, config, api_auth_token): Create a private directory beneath the given base directory, point the given config at it, and write the given API auth token to it. """ - FilePath(basedir).child(b"private").makedirs() + FilePath(basedir).child(u"private").makedirs() config._basedir = basedir - config.write_private_config(b"api_auth_token", api_auth_token) + config.write_private_config(u"api_auth_token", api_auth_token) class FromConfigurationTests(TestCase): @@ -299,7 +301,7 @@ class FromConfigurationTests(TestCase): the public keys found in the configuration. """ tempdir = self.useFixture(TempDir()) - config = get_config(tempdir.join(b"tahoe"), b"tub.port") + config = get_config(tempdir.join("tahoe"), "tub.port") allowed_public_keys = get_configured_allowed_public_keys(config) # root_from_config is just an easier way to call from_configuration @@ -323,24 +325,24 @@ class GetTokenCountTests(TestCase): ``get_token_count`` returns the integer value of the ``default-token-count`` item from the given configuration object. """ - plugin_name = u"hello-world" + plugin_name = "hello-world" if token_count is None: expected_count = NUM_TOKENS token_config = {} else: expected_count = token_count - token_config = {u"default-token-count": u"{}".format(expected_count)} + token_config = {"default-token-count": f"{expected_count}"} config_text = config_string_from_sections( [ { - u"storageclient.plugins." + plugin_name: token_config, + "storageclient.plugins." + plugin_name: token_config, } ] ) node_config = config_from_string( - self.useFixture(TempDir()).join(b"tahoe"), - u"tub.port", + self.useFixture(TempDir()).join("tahoe"), + "tub.port", config_text.encode("utf-8"), ) self.assertThat( @@ -364,7 +366,7 @@ class ResourceTests(TestCase): receives a 401 response. """ tempdir = self.useFixture(TempDir()) - config = get_config(tempdir.join(b"tahoe"), b"tub.port") + config = get_config(tempdir.join("tahoe"), "tub.port") root = root_from_config(config, datetime.now) agent = RequestTraversalAgent(root) requesting = agent.request( @@ -402,7 +404,7 @@ class ResourceTests(TestCase): ``from_configuration``. """ tempdir = self.useFixture(TempDir()) - config = get_config(tempdir.join(b"tahoe"), b"tub.port") + config = get_config(tempdir.join("tahoe"), "tub.port") root = root_from_config(config, datetime.now) self.assertThat( getChildForRequest(root, request), @@ -490,9 +492,9 @@ class UnblindedTokenTests(TestCase): root = root_from_config(config, datetime.now) agent = RequestTraversalAgent(root) data = BytesIO( - dumps( + dumps_utf8( { - u"unblinded-tokens": list( + "unblinded-tokens": list( token.unblinded_token.decode("ascii") for token in unblinded_tokens ) @@ -514,11 +516,15 @@ class UnblindedTokenTests(TestCase): ), ) - stored_tokens = root.controller.store.backup()[u"unblinded-tokens"] + stored_tokens = root.controller.store.backup()["unblinded-tokens"] self.assertThat( stored_tokens, - Equals(list(token.unblinded_token for token in unblinded_tokens)), + Equals( + list( + token.unblinded_token.decode("ascii") for token in unblinded_tokens + ) + ), ) @given( @@ -560,8 +566,8 @@ class UnblindedTokenTests(TestCase): b"http://127.0.0.1/unblinded-token", ) self.addDetail( - u"requesting result", - text_content(u"{}".format(vars(requesting.result))), + "requesting result", + text_content(f"{vars(requesting.result)}"), ) self.assertThat( requesting, @@ -605,11 +611,11 @@ class UnblindedTokenTests(TestCase): api_auth_token, agent, b"GET", - b"http://127.0.0.1/unblinded-token?limit={}".format(limit), + u"http://127.0.0.1/unblinded-token?limit={}".format(limit).encode("utf-8"), ) self.addDetail( - u"requesting result", - text_content(u"{}".format(vars(requesting.result))), + "requesting result", + text_content(f"{vars(requesting.result)}"), ) self.assertThat( requesting, @@ -658,13 +664,13 @@ class UnblindedTokenTests(TestCase): api_auth_token, agent, b"GET", - b"http://127.0.0.1/unblinded-token?position={}".format( + u"http://127.0.0.1/unblinded-token?position={}".format( quote(position.encode("utf-8"), safe=b""), - ), + ).encode("utf-8"), ) self.addDetail( - u"requesting result", - text_content(u"{}".format(vars(requesting.result))), + "requesting result", + text_content(f"{vars(requesting.result)}"), ) self.assertThat( requesting, @@ -674,7 +680,7 @@ class UnblindedTokenTests(TestCase): AllMatch( MatchesAll( GreaterThan(position), - IsInstance(unicode), + IsInstance(str), ), ), matches_lease_maintenance_spending(), @@ -715,7 +721,7 @@ class UnblindedTokenTests(TestCase): ) d.addCallback(readBody) d.addCallback( - lambda body: loads(body)[u"unblinded-tokens"], + lambda body: loads(body)["unblinded-tokens"], ) return d @@ -756,7 +762,7 @@ class UnblindedTokenTests(TestCase): succeeded( MatchesPredicate( check_tokens, - u"initial, after (%s): initial[1:] != after", + "initial, after (%s): initial[1:] != after", ), ), ) @@ -803,7 +809,7 @@ class UnblindedTokenTests(TestCase): ) d.addCallback(readBody) d.addCallback( - lambda body: loads(body)[u"lease-maintenance-spending"], + lambda body: loads(body)["lease-maintenance-spending"], ) self.assertThat( d, @@ -845,10 +851,10 @@ def succeeded_with_unblinded_tokens_with_matcher( 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, + "total": Equals(all_token_count), + "spendable": match_spendable_token_count, + "unblinded-tokens": match_unblinded_tokens, + "lease-maintenance-spending": match_lease_maint_spending, } ), ), @@ -873,7 +879,7 @@ def succeeded_with_unblinded_tokens(all_token_count, returned_token_count): match_spendable_token_count=Equals(all_token_count), match_unblinded_tokens=MatchesAll( HasLength(returned_token_count), - AllMatch(IsInstance(unicode)), + AllMatch(IsInstance(str)), ), match_lease_maint_spending=matches_lease_maintenance_spending(), ) @@ -889,8 +895,8 @@ def matches_lease_maintenance_spending(): Is(None), ContainsDict( { - u"when": matches_iso8601_datetime(), - u"amount": matches_positive_integer(), + "when": matches_iso8601_datetime(), + "amount": matches_positive_integer(), } ), ) @@ -905,11 +911,11 @@ def matches_positive_integer(): def matches_iso8601_datetime(): """ - :return: A matcher which matches unicode strings which can be parsed as an + :return: A matcher which matches text strings which can be parsed as an ISO8601 datetime string. """ return MatchesAll( - IsInstance(unicode), + IsInstance(str), AfterPreprocessing( parse_datetime, lambda d: Always(), @@ -942,7 +948,7 @@ class VoucherTests(TestCase): ) root = root_from_config(config, datetime.now) agent = RequestTraversalAgent(root) - data = BytesIO(dumps({u"voucher": voucher.decode("ascii")})) + data = BytesIO(dumps_utf8({"voucher": voucher.decode("ascii")})) requesting = authorized_request( api_auth_token, agent, @@ -951,8 +957,8 @@ class VoucherTests(TestCase): data=data, ) self.addDetail( - u"requesting result", - text_content(u"{}".format(vars(requesting.result))), + "requesting result", + text_content(f"{vars(requesting.result)}"), ) self.assertThat( requesting, @@ -983,8 +989,8 @@ class VoucherTests(TestCase): data=BytesIO(body), ) self.addDetail( - u"requesting result", - text_content(u"{}".format(vars(requesting.result))), + "requesting result", + text_content(f"{vars(requesting.result)}"), ) self.assertThat( requesting, @@ -1008,9 +1014,9 @@ class VoucherTests(TestCase): agent = RequestTraversalAgent(root) url = u"http://127.0.0.1/voucher/{}".format( quote( - not_voucher.encode("utf-8"), + not_voucher, safe=b"", - ).decode("utf-8"), + ), ).encode("ascii") requesting = authorized_request( api_auth_token, @@ -1043,7 +1049,7 @@ class VoucherTests(TestCase): api_auth_token, agent, b"GET", - u"http://127.0.0.1/voucher/{}".format(voucher).encode("ascii"), + b"http://127.0.0.1/voucher/" + voucher, ) self.assertThat( requesting, @@ -1224,7 +1230,7 @@ class VoucherTests(TestCase): to be returned by the ``GET``. """ add_api_token_to_config( - self.useFixture(TempDir()).join(b"tahoe"), + self.useFixture(TempDir()).join("tahoe"), config, api_auth_token, ) @@ -1235,7 +1241,7 @@ class VoucherTests(TestCase): agent, b"PUT", b"http://127.0.0.1/voucher", - data=BytesIO(dumps({u"voucher": voucher.decode("ascii")})), + data=BytesIO(dumps_utf8({"voucher": voucher.decode("ascii")})), ) self.assertThat( putting, @@ -1250,9 +1256,9 @@ class VoucherTests(TestCase): b"GET", u"http://127.0.0.1/voucher/{}".format( quote( - voucher.encode("utf-8"), - safe=b"", - ).decode("utf-8"), + voucher, + safe=u"", + ), ).encode("ascii"), ) self.assertThat( @@ -1292,7 +1298,7 @@ class VoucherTests(TestCase): vouchers, Equals( { - u"vouchers": list( + "vouchers": list( Voucher( number=voucher, expected_tokens=count, @@ -1329,7 +1335,7 @@ class VoucherTests(TestCase): vouchers, Equals( { - u"vouchers": list( + "vouchers": list( Voucher( number=voucher, expected_tokens=count, @@ -1352,7 +1358,7 @@ class VoucherTests(TestCase): # times between setUp and tearDown. Avoid re-using the same # temporary directory for every Hypothesis iteration because this # test leaves state behind that invalidates future iterations. - self.useFixture(TempDir()).join(b"tahoe"), + self.useFixture(TempDir()).join("tahoe"), config, api_auth_token, ) @@ -1362,7 +1368,7 @@ class VoucherTests(TestCase): note("{} vouchers".format(len(vouchers))) for voucher in vouchers: - data = BytesIO(dumps({u"voucher": voucher.decode("ascii")})) + data = BytesIO(dumps_utf8({"voucher": voucher.decode("ascii")})) putting = authorized_request( api_auth_token, agent, @@ -1400,12 +1406,11 @@ class VoucherTests(TestCase): ) -def mime_types(blacklist=None): +def mime_types(blacklist: Optional[Set[str]] = None) -> SearchStrategy[str]: """ Build MIME types as b"major/minor" byte strings. - :param set|None blacklist: If not ``None``, MIME types to exclude from the - result. + :param blacklist: If not ``None``, MIME types to exclude from the result. """ if blacklist is None: blacklist = set() @@ -1415,7 +1420,7 @@ def mime_types(blacklist=None): text(), ) .map( - b"/".join, + u"/".join, ) .filter( lambda content_type: content_type not in blacklist, @@ -1455,7 +1460,7 @@ def bad_calculate_price_requests(): bad_headers = fixed_dictionaries( { b"content-type": mime_types(blacklist={b"application/json"},).map( - lambda content_type: [content_type], + lambda content_type: [content_type.encode("utf-8")], ), } ) @@ -1479,29 +1484,29 @@ def bad_calculate_price_requests(): good_data = fixed_dictionaries( { - u"version": good_version, - u"sizes": good_sizes, + "version": good_version, + "sizes": good_sizes, } - ).map(dumps) + ).map(dumps_utf8) bad_data_version = fixed_dictionaries( { - u"version": bad_version, - u"sizes": good_sizes, + "version": bad_version, + "sizes": good_sizes, } - ).map(dumps) + ).map(dumps_utf8) bad_data_sizes = fixed_dictionaries( { - u"version": good_version, - u"sizes": bad_sizes, + "version": good_version, + "sizes": bad_sizes, } - ).map(dumps) + ).map(dumps_utf8) bad_data_other = dictionaries( text(), integers(), - ).map(dumps) + ).map(dumps_utf8) bad_data_junk = binary() @@ -1618,7 +1623,7 @@ class CalculatePriceTests(TestCase): (encoding_params, min_time_remaining), config = encoding_params_and_config shares_needed, shares_happy, shares_total = encoding_params add_api_token_to_config( - self.useFixture(TempDir()).join(b"tahoe"), + self.useFixture(TempDir()).join("tahoe"), config, api_auth_token, ) @@ -1638,7 +1643,7 @@ class CalculatePriceTests(TestCase): b"POST", self.url, headers={b"content-type": [b"application/json"]}, - data=BytesIO(dumps({u"version": 1, u"sizes": sizes})), + data=BytesIO(dumps_utf8({"version": 1, "sizes": sizes})), ), succeeded( matches_response( @@ -1648,8 +1653,8 @@ class CalculatePriceTests(TestCase): loads, Equals( { - u"price": expected_price, - u"period": 60 * 60 * 24 * 31 - min_time_remaining, + "price": expected_price, + "period": 60 * 60 * 24 * 31 - min_time_remaining, } ), ), @@ -1660,8 +1665,8 @@ class CalculatePriceTests(TestCase): def application_json(): return AfterPreprocessing( - lambda h: h.getRawHeaders(u"content-type"), - Equals([u"application/json"]), + lambda h: h.getRawHeaders("content-type"), + Equals(["application/json"]), ) @@ -1672,7 +1677,7 @@ def json_content(response): def ok_response(headers=None): - return match_response(OK, headers) + return match_response(OK, headers, phrase=Equals(b"OK")) def not_found_response(headers=None): @@ -1683,12 +1688,13 @@ def bad_request_response(headers=None): return match_response(BAD_REQUEST, headers) -def match_response(code, headers): +def match_response(code, headers, phrase=Always()): if headers is None: headers = Always() return _MatchResponse( code=Equals(code), headers=headers, + phrase=phrase, ) @@ -1696,18 +1702,20 @@ def match_response(code, headers): class _MatchResponse(object): code = attr.ib() headers = attr.ib() + phrase = attr.ib() _details = attr.ib(default=attr.Factory(dict)) def match(self, response): self._details.update( { - u"code": response.code, - u"headers": response.headers.getAllRawHeaders(), + "code": response.code, + "headers": response.headers.getAllRawHeaders(), } ) return MatchesStructure( code=self.code, headers=self.headers, + phrase=self.phrase, ).match(response) def get_details(self): diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py index e4f08b1ae621d7ca86d81a6698a768106eb1a176..f48b8e11d9f7f02441839660f9afee081bd4ff86 100644 --- a/src/_zkapauthorizer/tests/test_controller.py +++ b/src/_zkapauthorizer/tests/test_controller.py @@ -16,11 +16,9 @@ Tests for ``_zkapauthorizer.controller``. """ -from __future__ import absolute_import, division - from datetime import datetime, timedelta from functools import partial -from json import dumps, loads +from json import loads import attr from challenge_bypass_ristretto import ( @@ -42,13 +40,14 @@ from testtools.matchers import ( Always, Equals, HasLength, + Is, 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.defer import fail, succeed 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 @@ -57,6 +56,7 @@ from twisted.web.iweb import IAgent from twisted.web.resource import ErrorPage, Resource from zope.interface import implementer +from .._json import dumps_utf8 from ..controller import ( AlreadySpent, DoubleSpendRedeemer, @@ -72,6 +72,7 @@ from ..controller import ( Unpaid, UnpaidRedeemer, UnrecognizedFailureReason, + bracket, token_count_for_group, ) from ..model import DoubleSpend as model_DoubleSpend @@ -792,7 +793,9 @@ class RistrettoRedeemerTests(TestCase): HasLength(num_tokens), ), public_key=Equals( - PublicKey.from_signing_key(signing_key).encode_base64(), + PublicKey.from_signing_key(signing_key) + .encode_base64() + .decode("utf-8"), ), ), ), @@ -1151,12 +1154,12 @@ class RistrettoRedemption(Resource): finally: servers_proof.destroy() - return dumps( + return dumps_utf8( { u"success": True, - u"public-key": self.public_key.encode_base64(), - u"signatures": marshaled_signed_tokens, - u"proof": marshaled_proof, + u"public-key": self.public_key.encode_base64().decode("utf-8"), + u"signatures": list(t.decode("utf-8") for t in marshaled_signed_tokens), + u"proof": marshaled_proof.decode("utf-8"), } ) @@ -1229,7 +1232,7 @@ class CheckRedemptionRequestTests(TestCase): treq = treq_for_loopback_ristretto(issuer) d = treq.post( NOWHERE.child(u"v1", u"redeem").to_text().encode("ascii"), - dumps(dict.fromkeys(properties)), + dumps_utf8(dict.fromkeys(properties)), headers=Headers({u"content-type": [u"application/json"]}), ) self.assertThat( @@ -1248,7 +1251,7 @@ def check_redemption_request(request): Verify that the given request conforms to the redemption server's public interface. """ - if request.requestHeaders.getRawHeaders(b"content-type") != ["application/json"]: + if request.requestHeaders.getRawHeaders(b"content-type") != [b"application/json"]: return bad_content_type(request) p = request.content.tell() @@ -1279,7 +1282,7 @@ def check_redemption_request(request): def bad_request(request, body_object): request.setResponseCode(BAD_REQUEST) request.setHeader(b"content-type", b"application/json") - request.write(dumps(body_object)) + request.write(dumps_utf8(body_object)) return b"" @@ -1289,3 +1292,190 @@ def bad_content_type(request): b"Unsupported media type", b"Unsupported media type", ).render(request) + + +class _BracketTestMixin: + """ + Tests for ``bracket``. + """ + + def wrap_success(self, result): + raise NotImplementedError() + + def wrap_failure(self, result): + raise NotImplementedError() + + def test_success(self): + """ + ``bracket`` calls ``first`` then ``between`` then ``last`` and returns a + ``Deferred`` that fires with the result of ``between``. + """ + result = object() + actions = [] + first = partial(actions.append, "first") + + def between(): + actions.append("between") + return self.wrap_success(result) + + last = partial(actions.append, "last") + self.assertThat( + bracket(first, last, between), + succeeded( + Is(result), + ), + ) + self.assertThat( + actions, + Equals(["first", "between", "last"]), + ) + + def test_failure(self): + """ + ``bracket`` calls ``first`` then ``between`` then ``last`` and returns a + ``Deferred`` that fires with the failure result of ``between``. + """ + + class SomeException(Exception): + pass + + actions = [] + first = partial(actions.append, "first") + + def between(): + actions.append("between") + return self.wrap_failure(SomeException()) + + last = partial(actions.append, "last") + self.assertThat( + bracket(first, last, between), + failed( + AfterPreprocessing( + lambda failure: failure.value, + IsInstance(SomeException), + ), + ), + ) + self.assertThat( + actions, + Equals(["first", "between", "last"]), + ) + + def test_success_with_failing_last(self): + """ + If the ``between`` action succeeds and the ``last`` action fails then + ``bracket`` fails the same way as the ``last`` action. + """ + + class SomeException(Exception): + pass + + actions = [] + first = partial(actions.append, "first") + + def between(): + actions.append("between") + return self.wrap_success(None) + + def last(): + actions.append("last") + return self.wrap_failure(SomeException()) + + self.assertThat( + bracket(first, last, between), + failed( + AfterPreprocessing( + lambda failure: failure.value, + IsInstance(SomeException), + ), + ), + ) + self.assertThat( + actions, + Equals(["first", "between", "last"]), + ) + + def test_failure_with_failing_last(self): + """ + If both the ``between`` and ``last`` actions fail then ``bracket`` fails + the same way as the ``last`` action. + """ + + class SomeException(Exception): + pass + + class AnotherException(Exception): + pass + + actions = [] + first = partial(actions.append, "first") + + def between(): + actions.append("between") + return self.wrap_failure(SomeException()) + + def last(): + actions.append("last") + return self.wrap_failure(AnotherException()) + + self.assertThat( + bracket(first, last, between), + failed( + AfterPreprocessing( + lambda failure: failure.value, + IsInstance(AnotherException), + ), + ), + ) + self.assertThat( + actions, + Equals(["first", "between", "last"]), + ) + + def test_first_failure(self): + """ + If the ``first`` action fails then ``bracket`` fails the same way and + runs neither the ``between`` nor ``last`` actions. + """ + + class SomeException(Exception): + pass + + actions = [] + + def first(): + actions.append("first") + return self.wrap_failure(SomeException()) + + between = partial(actions.append, "between") + last = partial(actions.append, "last") + + self.assertThat( + bracket(first, last, between), + failed( + AfterPreprocessing( + lambda failure: failure.value, + IsInstance(SomeException), + ), + ), + ) + self.assertThat( + actions, + Equals(["first"]), + ) + + +class BracketTests(_BracketTestMixin, TestCase): + def wrap_success(self, result): + return result + + def wrap_failure(self, exception): + raise exception + + +class SynchronousDeferredBracketTests(_BracketTestMixin, TestCase): + def wrap_success(self, result): + return succeed(result) + + def wrap_failure(self, exception): + return fail(exception) diff --git a/src/_zkapauthorizer/tests/test_foolscap.py b/src/_zkapauthorizer/tests/test_foolscap.py index 3a313b879aa720caf31b20ce97b191d9289e1424..f22985f418ba29f73c98af81fe9103271b05784d 100644 --- a/src/_zkapauthorizer/tests/test_foolscap.py +++ b/src/_zkapauthorizer/tests/test_foolscap.py @@ -16,8 +16,6 @@ Tests for Foolscap-related test helpers. """ -from __future__ import absolute_import - from fixtures import Fixture from foolscap.api import Any, RemoteInterface, Violation from foolscap.furl import decode_furl @@ -55,7 +53,7 @@ class IHasSchema(RemoteInterface): def remote_reference(): tub = Tub() tub.setLocation("127.0.0.1:12345") - url = tub.buildURL(b"efgh") + url = tub.buildURL("efgh") # Ugh ugh ugh. Skip over the extra correctness checking in # RemoteReferenceTracker.__init__ that requires having a broker by passing @@ -86,7 +84,7 @@ class LocalRemoteTests(TestCase): self.assertThat( ref.tracker.getURL(), MatchesAll( - IsInstance(bytes), + IsInstance(str), AfterPreprocessing( decode_furl, Always(), diff --git a/src/_zkapauthorizer/tests/test_lease_maintenance.py b/src/_zkapauthorizer/tests/test_lease_maintenance.py index 02e2230c2ae2b6ad9b4d97e7c9149aeceb928722..703305b19cdd241028f42de49b6516efaf0e98e0 100644 --- a/src/_zkapauthorizer/tests/test_lease_maintenance.py +++ b/src/_zkapauthorizer/tests/test_lease_maintenance.py @@ -16,9 +16,8 @@ Tests for ``_zkapauthorizer.lease_maintenance``. """ -from __future__ import absolute_import, unicode_literals - from datetime import datetime, timedelta +from typing import Dict, List import attr from allmydata.client import SecretHolder @@ -78,12 +77,6 @@ from .strategies import ( storage_indexes, ) -try: - from typing import Dict, List -except ImportError: - pass - - default_lease_maint_config = lease_maintenance_from_tahoe_config(empty_config) @@ -284,8 +277,8 @@ class LeaseMaintenanceServiceTests(TestCase): [maintenance_call] = clock.getDelayedCalls() datetime_now = datetime.utcfromtimestamp(clock.seconds()) - low = datetime_now + mean - (range_ / 2) - high = datetime_now + mean + (range_ / 2) + low = datetime_now + mean - (range_ // 2) + high = datetime_now + mean + (range_ // 2) self.assertThat( datetime.utcfromtimestamp(maintenance_call.getTime()), between(low, high), @@ -313,7 +306,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("last-run")) - last_run_path.setContent(last_run.isoformat()) + last_run_path.setContent(last_run.isoformat().encode("utf-8")) service = lease_maintenance_service( dummy_maintain_leases, @@ -331,14 +324,14 @@ class LeaseMaintenanceServiceTests(TestCase): low = datetime_now + max( timedelta(0), - mean - (range_ / 2) - since_last_run, + mean - (range_ // 2) - since_last_run, ) high = max( # If since_last_run is one microsecond (precision of timedelta) # then the range is indivisible. Avoid putting the expected high # below the expected low. low, - datetime_now + mean + (range_ / 2) - since_last_run, + datetime_now + mean + (range_ // 2) - since_last_run, ) note( diff --git a/src/_zkapauthorizer/tests/test_matchers.py b/src/_zkapauthorizer/tests/test_matchers.py index e34bb8ab63f8c98d0dc1b63aaab26d9f45589119..d97ac1a16de5465357629cbac314cda4d8d36e36 100644 --- a/src/_zkapauthorizer/tests/test_matchers.py +++ b/src/_zkapauthorizer/tests/test_matchers.py @@ -16,8 +16,6 @@ Tests for ``_zkapauthorizer.tests.matchers``. """ -from __future__ import absolute_import - from testtools import TestCase from testtools.matchers import Is, Not from zope.interface import Interface, implementer diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py index 93605cd83f2f373021707a0edb5cf7d49df81024..f5d195f21a005b1f42e2fc8f30ef640334577fa5 100644 --- a/src/_zkapauthorizer/tests/test_model.py +++ b/src/_zkapauthorizer/tests/test_model.py @@ -17,8 +17,6 @@ Tests for ``_zkapauthorizer.model``. """ -from __future__ import absolute_import - from datetime import datetime, timedelta from errno import EACCES from os import mkdir @@ -141,8 +139,8 @@ class VoucherStoreTests(TestCase): """ 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 @@ -253,13 +251,13 @@ class VoucherStoreTests(TestCase): then ``VoucherStore.from_node_config`` raises ``StoreOpenError``. """ tempdir = self.useFixture(TempDir()) - nodedir = tempdir.join(b"node") + nodedir = tempdir.join(u"node") # Create the node directory without permission to create the # underlying directory. mkdir(nodedir, 0o500) - config = get_config(nodedir, b"tub.port") + config = get_config(nodedir, u"tub.port") self.assertThat( lambda: VoucherStore.from_node_config( @@ -269,7 +267,7 @@ class VoucherStoreTests(TestCase): ), Raises( AfterPreprocessing( - lambda (type, exc, tb): exc, + lambda exc_info: exc_info[1], MatchesAll( IsInstance(StoreOpenError), MatchesStructure( @@ -295,9 +293,9 @@ class VoucherStoreTests(TestCase): ``VoucherStore.from_node_config`` raises ``StoreOpenError``. """ tempdir = self.useFixture(TempDir()) - nodedir = tempdir.join(b"node") + nodedir = tempdir.join(u"node") - config = get_config(nodedir, b"tub.port") + config = get_config(nodedir, u"tub.port") # Create the underlying database file. store = VoucherStore.from_node_config(config, lambda: now) @@ -358,9 +356,9 @@ class VoucherStoreTests(TestCase): :return: A three-tuple of (backed up tokens, extracted tokens, inserted tokens). """ tempdir = self.useFixture(TempDir()) - nodedir = tempdir.join(b"node") + nodedir = tempdir.join(u"node") - config = get_config(nodedir, b"tub.port") + config = get_config(nodedir, u"tub.port") # Create the underlying database file. store = VoucherStore.from_node_config(config, lambda: now) @@ -384,14 +382,15 @@ 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.decode("ascii") + for token in store.get_unblinded_tokens(to_spend) ) tokens_remaining -= to_spend return ( backed_up_tokens, extracted_tokens, - list(token.unblinded_token for token in unblinded_tokens), + list(token.unblinded_token.decode("ascii") for token in unblinded_tokens), ) @@ -946,7 +945,7 @@ def store_for_test(testcase, get_config, get_now): :return VoucherStore: A newly created temporary store. """ tempdir = testcase.useFixture(TempDir()) - config = get_config(tempdir.join(b"node"), b"tub.port") + config = get_config(tempdir.join(u"node"), u"tub.port") store = VoucherStore.from_node_config( config, get_now, diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py index f300d3a6ece15d8ba48479bb437c300a2057fc0d..27e2a5a679cdb5c4260bb9101f02ad3b616e8171 100644 --- a/src/_zkapauthorizer/tests/test_plugin.py +++ b/src/_zkapauthorizer/tests/test_plugin.py @@ -16,21 +16,21 @@ Tests for the Tahoe-LAFS plugin. """ -from __future__ import absolute_import - from datetime import timedelta from functools import partial +from io import StringIO from os import makedirs from allmydata.client import config_from_string, create_client_from_config from allmydata.interfaces import ( IAnnounceableStorageServer, + IFilesystemNode, IFoolscapStoragePlugin, IStorageServer, RIStorageServer, ) from challenge_bypass_ristretto import SigningKey -from eliot.testing import LoggedMessage +from eliot.testing import LoggedMessage, capture_logging from fixtures import TempDir from foolscap.broker import Broker from foolscap.ipb import IReferenceable, IRemotelyCallable @@ -39,7 +39,6 @@ from hypothesis import given, settings from hypothesis.strategies import datetimes, just, sampled_from, timedeltas from prometheus_client import Gauge from prometheus_client.parser import text_string_to_metric_families -from StringIO import StringIO from testtools import TestCase from testtools.content import text_content from testtools.matchers import ( @@ -68,7 +67,7 @@ from twisted.web.resource import IResource from twisted.plugins.zkapauthorizer import storage_server -from .._plugin import load_signing_key +from .._plugin import get_root_nodes, load_signing_key from .._storage_client import IncorrectStorageServerReference from ..controller import DummyRedeemer, IssuerConfigurationMismatch, PaymentController from ..foolscap import RIPrivacyPassAuthorizedStorageServer @@ -76,7 +75,6 @@ from ..lease_maintenance import SERVICE_NAME, LeaseMaintenanceConfig from ..model import NotEnoughTokens, VoucherStore from ..spending import GET_PASSES from .common import skipIf -from .eliot import capture_logging from .foolscap import DummyReferenceable, LocalRemote, get_anonymous_storage_server from .matchers import Provides, raises from .strategies import ( @@ -99,7 +97,7 @@ from .strategies import ( vouchers, ) -SIGNING_KEY_PATH = FilePath(__file__).sibling(u"testing-signing.key") +SIGNING_KEY_PATH = FilePath(__file__).sibling("testing-signing.key") def get_rref(interface=None): @@ -281,12 +279,12 @@ class ServerPluginTests(TestCase): and an interval how often to do so, test that metrics are actually written there after the configured interval. """ - metrics_path = self.useFixture(TempDir()).join(u"metrics") + metrics_path = self.useFixture(TempDir()).join("metrics") configuration = { - u"prometheus-metrics-path": metrics_path, - u"prometheus-metrics-interval": str(int(metrics_interval.total_seconds())), - u"ristretto-issuer-root-url": "foo", - u"ristretto-signing-key-path": SIGNING_KEY_PATH.path, + "prometheus-metrics-path": metrics_path, + "prometheus-metrics-interval": str(int(metrics_interval.total_seconds())), + "ristretto-issuer-root-url": "foo", + "ristretto-signing-key-path": SIGNING_KEY_PATH.path, } announceable = extract_result( storage_server.get_storage_server( @@ -341,8 +339,8 @@ tahoe_configs_with_dummy_redeemer = tahoe_configs(client_dummyredeemer_configura tahoe_configs_with_mismatched_issuer = minimal_tahoe_configs( { - u"privatestorageio-zkapauthz-v1": just( - {u"ristretto-issuer-root-url": u"https://another-issuer.example.invalid/"} + "privatestorageio-zkapauthz-v1": just( + {"ristretto-issuer-root-url": "https://another-issuer.example.invalid/"} ), } ) @@ -362,8 +360,8 @@ class ClientPluginTests(TestCase): """ tempdir = self.useFixture(TempDir()) node_config = get_config( - tempdir.join(b"node"), - b"tub.port", + tempdir.join(u"node"), + u"tub.port", ) storage_client = storage_server.get_storage_client( @@ -385,24 +383,14 @@ class ClientPluginTests(TestCase): """ tempdir = self.useFixture(TempDir()) node_config = config_from_string( - tempdir.join(b"node"), - b"tub.port", + tempdir.join(u"node"), + u"tub.port", config_text.encode("utf-8"), ) - # On Tahoe-LAFS <1.16, the config is written as bytes. - # On Tahoe-LAFS >=1.16, the config is written as unicode. - # - # So we'll use `StringIO.StringIO` (not `io.StringIO`) here - which - # will allow either type (it will also implicitly decode bytes to - # unicode if we mix them, though I don't think that should happen - # here). - # - # After support for Tahoe <1.16 support is dropped we probably want to - # switch to an io.StringIO here. config_text = StringIO() node_config.config.write(config_text) - self.addDetail(u"config", text_content(config_text.getvalue())) - self.addDetail(u"announcement", text_content(unicode(announcement))) + self.addDetail("config", text_content(config_text.getvalue())) + self.addDetail("announcement", text_content(str(announcement))) self.assertThat( lambda: storage_server.get_storage_client( node_config, @@ -439,8 +427,8 @@ class ClientPluginTests(TestCase): """ tempdir = self.useFixture(TempDir()) node_config = get_config( - tempdir.join(b"node"), - b"tub.port", + tempdir.join(u"node"), + u"tub.port", ) storage_client = storage_server.get_storage_client( @@ -489,8 +477,8 @@ class ClientPluginTests(TestCase): """ tempdir = self.useFixture(TempDir()) node_config = get_config( - tempdir.join(b"node"), - b"tub.port", + tempdir.join(u"node"), + u"tub.port", ) store = VoucherStore.from_node_config(node_config, lambda: now) @@ -521,12 +509,12 @@ class ClientPluginTests(TestCase): # tests, at least until creating a real server doesn't involve so much # complex setup. So avoid using any of the client APIs that make a # remote call ... which is all of them. - pass_group = storage_client._get_passes(u"request binding message", num_passes) + pass_group = storage_client._get_passes(b"request binding message", num_passes) pass_group.mark_spent() # There should be no unblinded tokens left to extract. self.assertThat( - lambda: storage_client._get_passes(u"request binding message", 1), + lambda: storage_client._get_passes(b"request binding message", 1), raises(NotEnoughTokens), ) @@ -540,8 +528,8 @@ class ClientPluginTests(TestCase): lambda logged_message: logged_message.message, ContainsDict( { - u"message": Equals(u"request binding message"), - u"count": Equals(num_passes), + "message": Equals(u"request binding message"), + "count": Equals(num_passes), } ), ), @@ -562,8 +550,8 @@ class ClientResourceTests(TestCase): ``get_client_resource`` returns an object that provides ``IResource``. """ tempdir = self.useFixture(TempDir()) - nodedir = tempdir.join(b"node") - config = get_config(nodedir, b"tub.port") + nodedir = tempdir.join(u"node") + config = get_config(nodedir, u"tub.port") self.assertThat( storage_server.get_client_resource( config, @@ -625,25 +613,63 @@ class LeaseMaintenanceServiceTests(TestCase): file, ``False`` otherwise. """ tempdir = self.useFixture(TempDir()) - nodedir = tempdir.join(b"node") - privatedir = tempdir.join(b"node", b"private") + nodedir = tempdir.join(u"node") + privatedir = tempdir.join(u"node", u"private") makedirs(privatedir) - config = get_config(nodedir, b"tub.port") + config = get_config(nodedir, u"tub.port") + + # In Tahoe-LAFS 1.17 write_private_config is broken. It mixes bytes + # and unicode in an os.path.join() call that always fails with a + # TypeError. + def write_private_config(name, value): + privpath = FilePath(config._basedir).descendant([u"private", name]) + privpath.setContent(value) if servers_yaml is not None: # Provide it a statically configured server to connect to. - config.write_private_config( - b"servers.yaml", + write_private_config( + u"servers.yaml", servers_yaml, ) if rootcap: config.write_private_config( - b"rootcap", + u"rootcap", b"dddddddd", ) return create_client_from_config(config) + @given(tahoe_configs()) + def test_get_root_nodes_rootcap_present(self, get_config): + """ + ``get_root_nodes`` returns a ``list`` of one ``IFilesystemNode`` provider + derived from the contents of the *rootcap* private configuration. + """ + d = self._create(get_config, servers_yaml=None, rootcap=True) + client_node = extract_result(d) + roots = get_root_nodes(client_node, client_node.config) + self.assertThat( + roots, + MatchesAll( + HasLength(1), + AllMatch(Provides([IFilesystemNode])), + ), + ) + + @given(tahoe_configs()) + def test_get_root_nodes_rootcap_missing(self, get_config): + """ + ``get_root_nodes`` returns an empty ``list`` if there is no private + *rootcap* configuration. + """ + d = self._create(get_config, servers_yaml=None, rootcap=False) + client_node = extract_result(d) + roots = get_root_nodes(client_node, client_node.config) + self.assertThat( + roots, + Equals([]), + ) + @settings( deadline=None, ) @@ -753,7 +779,7 @@ class LoadSigningKeyTests(TestCase): :param bytes key: A base64-encoded Ristretto signing key. """ - p = FilePath(self.useFixture(TempDir()).join(b"key")) + p = FilePath(self.useFixture(TempDir()).join(u"key")) p.setContent(key_bytes) key = load_signing_key(p) self.assertThat(key, IsInstance(SigningKey)) diff --git a/src/_zkapauthorizer/tests/test_pricecalculator.py b/src/_zkapauthorizer/tests/test_pricecalculator.py index baadd9119d73a37988d90baa5bdb62f106405e73..c1652f2b4bcf4550a141c87615437e36ed3b4b98 100644 --- a/src/_zkapauthorizer/tests/test_pricecalculator.py +++ b/src/_zkapauthorizer/tests/test_pricecalculator.py @@ -155,7 +155,7 @@ class PriceCalculatorTests(TestCase): self.assertThat( price, MatchesAll( - IsInstance((int, long)), + IsInstance(int), GreaterThan(0), ), ) diff --git a/src/_zkapauthorizer/tests/test_private.py b/src/_zkapauthorizer/tests/test_private.py index 568cc1eb1baf613c17ee3336874b08b9aed4b17c..ba1fa34deed90454a4360c38464c3dcbe3a66645 100644 --- a/src/_zkapauthorizer/tests/test_private.py +++ b/src/_zkapauthorizer/tests/test_private.py @@ -9,8 +9,6 @@ Tests for ``_zkapauthorizer.private``. """ -from __future__ import absolute_import, division, print_function, unicode_literals - from allmydata.test.web.matchers import has_response_code from testtools import TestCase from testtools.matchers import Equals @@ -39,7 +37,9 @@ class PrivacyTests(TestCase): def _authorization(self, scheme, value): return Headers( { - "authorization": ["{} {}".format(scheme, value)], + u"authorization": [ + u"{} {}".format(scheme.decode("ascii"), value.decode("ascii")), + ], } ) @@ -60,7 +60,7 @@ class PrivacyTests(TestCase): self.assertThat( self.client.head( b"http:///foo/bar", - headers=self._authorization("basic", self.token), + headers=self._authorization(b"basic", self.token), ), succeeded(has_response_code(Equals(UNAUTHORIZED))), ) @@ -73,7 +73,7 @@ class PrivacyTests(TestCase): self.assertThat( self.client.head( b"http:///foo/bar", - headers=self._authorization(SCHEME, "foo bar"), + headers=self._authorization(SCHEME, b"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 1c53556018b064b4b47855d03242607ab8dd3aba..f0dc6f876a020bfbee26206eb5e3ad88958a1623 100644 --- a/src/_zkapauthorizer/tests/test_schema.py +++ b/src/_zkapauthorizer/tests/test_schema.py @@ -17,8 +17,6 @@ Tests for ``_zkapauthorizer.schema``. """ -from __future__ import absolute_import - from testtools import TestCase from testtools.matchers import Equals diff --git a/src/_zkapauthorizer/tests/test_spending.py b/src/_zkapauthorizer/tests/test_spending.py index 6833e0895e5d8f4ec38310eec5c25456ccc8e601..42a5d87f5c5d7fd9d860f28445051aa03e270972 100644 --- a/src/_zkapauthorizer/tests/test_spending.py +++ b/src/_zkapauthorizer/tests/test_spending.py @@ -62,7 +62,7 @@ class PassGroupTests(TestCase): store=configless.store, ) - group = pass_factory.get(u"message", num_passes) + group = pass_factory.get(b"message", num_passes) self.assertThat( group, MatchesAll( @@ -97,7 +97,7 @@ class PassGroupTests(TestCase): # Figure out some subset, maybe empty, of passes from the group that # we will try to operate on. group_size = data.draw(integers(min_value=0, max_value=num_passes)) - indices = range(num_passes) + indices = list(range(num_passes)) random.shuffle(indices) spent_indices = indices[:group_size] @@ -106,7 +106,7 @@ class PassGroupTests(TestCase): tokens_to_passes=configless.redeemer.tokens_to_passes, store=configless.store, ) - group = pass_factory.get(u"message", num_passes) + group = pass_factory.get(b"message", num_passes) spent, rest = group.split(spent_indices) operation(spent) diff --git a/src/_zkapauthorizer/tests/test_storage_client.py b/src/_zkapauthorizer/tests/test_storage_client.py index 1075884f570d7b15b220f5d33843c5105ecb522f..5a0658b61072c9119f2f518551d1c03d2605bc55 100644 --- a/src/_zkapauthorizer/tests/test_storage_client.py +++ b/src/_zkapauthorizer/tests/test_storage_client.py @@ -16,8 +16,6 @@ Tests for ``_zkapauthorizer._storage_client``. """ -from __future__ import division - from functools import partial from allmydata.client import config_from_string @@ -180,7 +178,7 @@ class CallWithPassesTests(TestCase): call_with_passes( lambda group: succeed(result), num_passes, - partial(pass_factory(integer_passes(num_passes)).get, u"message"), + partial(pass_factory(integer_passes(num_passes)).get, b"message"), ), succeeded(Is(result)), ) @@ -197,7 +195,7 @@ class CallWithPassesTests(TestCase): call_with_passes( lambda group: fail(result), num_passes, - partial(pass_factory(integer_passes(num_passes)).get, u"message"), + partial(pass_factory(integer_passes(num_passes)).get, b"message"), ), failed( AfterPreprocessing( @@ -220,7 +218,7 @@ class CallWithPassesTests(TestCase): call_with_passes( lambda group: succeed(group.passes), num_passes, - partial(passes.get, u"message"), + partial(passes.get, b"message"), ), succeeded( Equals( @@ -241,7 +239,7 @@ class CallWithPassesTests(TestCase): call_with_passes( lambda group: None, num_passes, - partial(passes.get, u"message"), + partial(passes.get, b"message"), ), succeeded(Always()), ) @@ -261,7 +259,7 @@ class CallWithPassesTests(TestCase): call_with_passes( lambda group: fail(Exception("Anything")), num_passes, - partial(passes.get, u"message"), + partial(passes.get, b"message"), ), failed(Always()), ) @@ -301,7 +299,7 @@ class CallWithPassesTests(TestCase): call_with_passes( reject_even_pass_values, num_passes, - partial(passes.get, u"message"), + partial(passes.get, b"message"), ), succeeded(Always()), ) @@ -342,7 +340,7 @@ class CallWithPassesTests(TestCase): call_with_passes( reject_passes, num_passes, - partial(passes.get, u"message"), + partial(passes.get, b"message"), ), failed( AfterPreprocessing( @@ -406,7 +404,7 @@ class CallWithPassesTests(TestCase): # out of passes no matter how many we start with. reject_half_passes, num_passes, - partial(passes.get, u"message"), + partial(passes.get, b"message"), ), failed( AfterPreprocessing( @@ -458,7 +456,7 @@ class PassFactoryTests(TestCase): ``IPassGroup.reset`` makes passes available to be returned by ``IPassGroup.get`` again. """ - message = u"message" + message = b"message" min_passes = min(num_passes_a, num_passes_b) max_passes = max(num_passes_a, num_passes_b) @@ -486,7 +484,7 @@ class PassFactoryTests(TestCase): :param (IPassGroup -> None) invalid_op: Some follow-up operation to perform with the pass group and to assert raises an exception. """ - message = u"message" + message = b"message" factory = pass_factory(integer_passes(num_passes)) group = factory.get(message, num_passes) setup_op(group) diff --git a/src/_zkapauthorizer/tests/test_storage_protocol.py b/src/_zkapauthorizer/tests/test_storage_protocol.py index 89654633b6b9176ec98fb71492571b0467eb42b7..ceb34deb3d1143a55f8072de31f658992ccce226 100644 --- a/src/_zkapauthorizer/tests/test_storage_protocol.py +++ b/src/_zkapauthorizer/tests/test_storage_protocol.py @@ -16,8 +16,6 @@ Tests for communication between the client and server components. """ -from __future__ import absolute_import - from allmydata.storage.common import storage_index_to_dir from allmydata.storage.shares import get_share_file from challenge_bypass_ristretto import PublicKey, random_signing_key @@ -72,6 +70,7 @@ from .storage_common import ( ) from .strategies import bytes_for_share # Not really a strategy... from .strategies import ( + TestAndWriteVectors, lease_cancel_secrets, lease_renew_secrets, posix_timestamps, @@ -79,6 +78,7 @@ from .strategies import ( sharenum_sets, sharenums, sizes, + slot_data_vectors, slot_test_and_write_vectors_for_shares, storage_indexes, write_enabler_secrets, @@ -1073,6 +1073,73 @@ class ShareTests(TestCase): ), ) + @given( + storage_index=storage_indexes(), + secrets=tuples( + write_enabler_secrets(), + lease_renew_secrets(), + lease_cancel_secrets(), + ), + sharenum=sharenums(), + data_vector=slot_data_vectors(), + replacement_data_vector=slot_data_vectors(), + ) + def test_test_vectors_match( + self, storage_index, secrets, sharenum, data_vector, replacement_data_vector + ): + """ + If test vectors are given then the write is allowed if they match the + existing data. + """ + empty_test_vector = [] + + def write(tw_vectors): + return self.client.slot_testv_and_readv_and_writev( + storage_index, + secrets=secrets, + tw_vectors=tw_vectors, + r_vector=[], + ) + + def read(sharenum, readv): + d = self.client.slot_readv(storage_index, [sharenum], readv) + d.addCallback(lambda data: data[sharenum]) + return d + + def equal_test_vector(data_vector): + return list((offset, len(data), data) for (offset, data) in data_vector) + + # Create the share + d = write( + { + sharenum: (empty_test_vector, data_vector, None), + } + ) + self.assertThat(d, is_successful_write()) + + # Write some new data with a correct test vector. We can only be sure + # we know data from the last element of the test vector since earlier + # elements may have been overwritten so only use that last element in + # our test vector. + d = write( + { + sharenum: ( + equal_test_vector(data_vector)[-1:], + replacement_data_vector, + None, + ), + } + ) + self.assertThat(d, is_successful_write()) + + # Check that the new data is present + assert_read_back_data( + self, + storage_index, + secrets, + {sharenum: TestAndWriteVectors(None, replacement_data_vector, None)}, + ) + def assert_read_back_data( self, storage_index, secrets, test_and_write_vectors_for_shares diff --git a/src/_zkapauthorizer/tests/test_storage_server.py b/src/_zkapauthorizer/tests/test_storage_server.py index 7e334d79e6f8fca00f16170d46dca9b2bd1f1513..73a0001fc38d5924d133d09682effa3c20727eb0 100644 --- a/src/_zkapauthorizer/tests/test_storage_server.py +++ b/src/_zkapauthorizer/tests/test_storage_server.py @@ -16,8 +16,6 @@ Tests for ``_zkapauthorizer._storage_server``. """ -from __future__ import absolute_import, division - from random import shuffle from time import time @@ -81,7 +79,7 @@ class ValidationResultTests(TestCase): ``validate_passes`` returns a ``_ValidationResult`` instance which describes the valid and invalid passes. """ - message = u"hello world" + message = b"hello world" valid_passes = get_passes( message, valid_count, @@ -136,7 +134,7 @@ class ValidationResultTests(TestCase): AfterPreprocessing( str, Equals( - "MorePassesRequired(valid_count=4, required_count=10, signature_check_failed=frozenset([4]))" + "MorePassesRequired(valid_count=4, required_count=10, signature_check_failed=frozenset({4}))", ), ), ), diff --git a/src/_zkapauthorizer/tests/test_strategies.py b/src/_zkapauthorizer/tests/test_strategies.py index b046450cc8289561a141b1e1e62b3979d4638692..1184dbbe0951ac80e759e799bd5c57b80e0d481e 100644 --- a/src/_zkapauthorizer/tests/test_strategies.py +++ b/src/_zkapauthorizer/tests/test_strategies.py @@ -16,8 +16,6 @@ Tests for our custom Hypothesis strategies. """ -from __future__ import absolute_import - from allmydata.client import config_from_string from fixtures import TempDir from hypothesis import given, note @@ -49,7 +47,7 @@ class TahoeConfigsTests(TestCase): ) note(config_text) config_from_string( - tempdir.join(b"tahoe.ini"), - b"tub.port", + tempdir.join(u"tahoe.ini"), + u"tub.port", config_text.encode("utf-8"), ) diff --git a/tests.nix b/tests.nix index 40412a752ecaacd918e0aad6bfab37def5f24383..0d7f6a7750c73da64c8f4619503394234497d3f9 100644 --- a/tests.nix +++ b/tests.nix @@ -51,9 +51,9 @@ let mkdir -p $out pushd ${zkapauthorizer.src} - ${python}/bin/pyflakes src ${lint-python}/bin/black --check src ${lint-python}/bin/isort --check src + ${lint-python}/bin/flake8 src popd ZKAPAUTHORIZER_HYPOTHESIS_PROFILE=${hypothesisProfile'} ${python}/bin/python -m ${if collectCoverage @@ -70,5 +70,5 @@ let ''; in { - inherit pkgs python lint-python tests; + inherit privatestorage lint-python tests; }