diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py index b16473f367d5be09920f3f399c33ecdba1fc95d7..1bc586e1d65f7ba536a0dd0f7e96c33facaa2650 100644 --- a/src/_zkapauthorizer/controller.py +++ b/src/_zkapauthorizer/controller.py @@ -114,6 +114,21 @@ class Unpaid(Exception): """ +@attr.s +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 unicode public_key: The public key which the server proved was + involved in the redemption process. + """ + unblinded_tokens = attr.ib() + public_key = attr.ib() + + class IRedeemer(Interface): """ An ``IRedeemer`` can exchange a voucher for one or more passes. @@ -150,12 +165,11 @@ class IRedeemer(Interface): :param list[RandomToken] random_tokens: The random tokens to use in the redemption process. - :return: A ``Deferred`` which fires with a list of ``UnblindedToken`` - instances on successful redemption or which fails with any error - to allow a retry to be made at some future point. It may also - fail with an ``AlreadySpent`` error to indicate the redemption - server considers the voucher to have been redeemed already and - will not allow it to be redeemed. + :return: A ``Deferred`` which fires with a ``RedemptionResult`` + instance or which fails with any error to allow a retry to be made + at some future point. It may also fail with an ``AlreadySpent`` + error to indicate the redemption server considers the voucher to + have been redeemed already and will not allow it to be redeemed. """ def tokens_to_passes(message, unblinded_tokens): @@ -290,6 +304,8 @@ class DummyRedeemer(object): really redeeming them, it makes up some fake ZKAPs and pretends those are the result. """ + _public_key = attr.ib(default=None) + @classmethod def make(cls, section_name, node_config, announcement, reactor): return cls() @@ -311,10 +327,13 @@ class DummyRedeemer(object): unblinded_value = random_value + b"x" * (96 - len(random_value)) return UnblindedToken(b64encode(unblinded_value).decode("ascii")) return succeed( - list( - dummy_unblinded_token(token) - for token - in random_tokens + RedemptionResult( + list( + dummy_unblinded_token(token) + for token + in random_tokens + ), + self._public_key, ), ) @@ -483,10 +502,14 @@ class RistrettoRedeemer(object): public_key, ) self._log.info("Validated proof") - returnValue(list( + unblinded_tokens = list( UnblindedToken(token.encode_base64().decode("ascii")) for token in clients_unblinded_tokens + ) + returnValue(RedemptionResult( + unblinded_tokens, + marshaled_public_key, )) def tokens_to_passes(self, message, unblinded_tokens): @@ -677,14 +700,21 @@ class PaymentController(object): tokens = self._get_random_tokens_for_voucher(voucher, num_tokens) return self._perform_redeem(voucher, tokens) - def _redeemSuccess(self, voucher, unblinded_tokens): + def _redeemSuccess(self, voucher, result): """ Update the database state to reflect that a voucher was redeemed and to store the resulting unblinded tokens (which can be used to construct passes later). """ - self._log.info("Inserting redeemed unblinded tokens for a voucher ({voucher}).", voucher=voucher) - self.store.insert_unblinded_tokens_for_voucher(voucher, unblinded_tokens) + self._log.info( + "Inserting redeemed unblinded tokens for a voucher ({voucher}).", + voucher=voucher, + ) + self.store.insert_unblinded_tokens_for_voucher( + voucher, + result.public_key, + result.unblinded_tokens, + ) def _redeemFailure(self, voucher, reason): if reason.check(AlreadySpent): diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py index a208fbc9cfc6e1db9a7a1b7bd8594e4da264d5fa..8691fface49804e256e2a003930cb9035c155da7 100644 --- a/src/_zkapauthorizer/model.py +++ b/src/_zkapauthorizer/model.py @@ -61,6 +61,12 @@ from .storage_common import ( required_passes, ) +from .schema import ( + get_schema_version, + get_schema_upgrades, + run_schema_upgrades, +) + class ILeaseMaintenanceObserver(Interface): """ @@ -88,13 +94,9 @@ class StoreOpenError(Exception): self.reason = reason -class SchemaError(TypeError): - pass - - CONFIG_DB_NAME = u"privatestorageio-zkapauthz-v1.sqlite3" -def open_and_initialize(path, required_schema_version, connect=None): +def open_and_initialize(path, connect=None): """ Open a SQLite3 database for use as a voucher store. @@ -103,13 +105,6 @@ def open_and_initialize(path, required_schema_version, connect=None): :param FilePath path: The location of the SQLite3 database file. - :param int required_schema_version: The schema version which must be - present in the database in order for a SQLite3 connection to be - returned. - - :raise SchemaError: If the schema in the database does not match the - required schema version. - :return: A SQLite3 connection object for the database at the given path. """ if connect is None: @@ -135,86 +130,9 @@ def open_and_initialize(path, required_schema_version, connect=None): with conn: cursor = conn.cursor() - cursor.execute( - # This code knows how to create schema version 1. This is - # regardless of what the caller *wants* to find in the database. - """ - CREATE TABLE IF NOT EXISTS [version] AS SELECT 1 AS [version] - """ - ) - cursor.execute( - """ - SELECT [version] FROM [version] - """ - ) - [(actual_version,)] = cursor.fetchall() - if actual_version != required_schema_version: - raise SchemaError( - "Unexpected database schema version. Required {}. Got {}.".format( - required_schema_version, - actual_version, - ), - ) - - cursor.execute( - # A denormalized schema because, for now, it's simpler. :/ - """ - CREATE TABLE IF NOT EXISTS [vouchers] ( - [number] text, - [created] text, -- An ISO8601 date+time string. - [state] text DEFAULT "pending", -- pending, double-spend, redeemed - - [finished] text DEFAULT NULL, -- ISO8601 date+time string when - -- the current terminal state was entered. - - [token-count] num DEFAULT NULL, -- Set in the redeemed state to the number - -- of tokens received on this voucher's - -- redemption. - - PRIMARY KEY([number]) - ) - """, - ) - cursor.execute( - """ - CREATE TABLE IF NOT EXISTS [tokens] ( - [text] text, -- The random string that defines the token. - [voucher] text, -- Reference to the voucher these tokens go with. - - PRIMARY KEY([text]) - FOREIGN KEY([voucher]) REFERENCES [vouchers]([number]) - ) - """, - ) - cursor.execute( - """ - CREATE TABLE IF NOT EXISTS [unblinded-tokens] ( - [token] text, -- The base64 encoded unblinded token. - - PRIMARY KEY([token]) - ) - """, - ) - cursor.execute( - """ - CREATE TABLE IF NOT EXISTS [lease-maintenance-spending] ( - [id] integer, -- A unique identifier for a group of activity. - [started] text, -- ISO8601 date+time string when the activity began. - [finished] text, -- ISO8601 date+time string when the activity completed (or null). - - -- The number of passes that would be required to renew all - -- shares encountered during this activity. Note that because - -- leases on different shares don't necessarily expire at the - -- same time this is not necessarily the number of passes - -- **actually** used during this activity. Some shares may - -- not have required lease renewal. Also note that while the - -- activity is ongoing this value may change. - [count] integer, - - PRIMARY KEY([id]) - ) - """, - ) + actual_version = get_schema_version(cursor) + schema_upgrades = list(get_schema_upgrades(actual_version)) + run_schema_upgrades(schema_upgrades, cursor) return conn @@ -268,7 +186,6 @@ class VoucherStore(object): db_path = FilePath(node_config.get_private_path(CONFIG_DB_NAME)) conn = open_and_initialize( db_path, - required_schema_version=1, connect=connect, ) return cls( @@ -287,7 +204,7 @@ class VoucherStore(object): cursor.execute( """ SELECT - [number], [created], [state], [finished], [token-count] + [number], [created], [state], [finished], [token-count], [public-key] FROM [vouchers] WHERE @@ -371,7 +288,7 @@ class VoucherStore(object): cursor.execute( """ SELECT - [number], [created], [state], [finished], [token-count] + [number], [created], [state], [finished], [token-count], [public-key] FROM [vouchers] """, @@ -385,7 +302,7 @@ class VoucherStore(object): ) @with_cursor - def insert_unblinded_tokens_for_voucher(self, cursor, voucher, unblinded_tokens): + def insert_unblinded_tokens_for_voucher(self, cursor, voucher, public_key, unblinded_tokens): """ Store some unblinded tokens. @@ -393,9 +310,32 @@ 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 + which was used to sign these tokens. + :param list[UnblindedToken] unblinded_tokens: The unblinded tokens to store. """ + voucher_state = u"redeemed" + cursor.execute( + """ + UPDATE [vouchers] + SET [state] = ? + , [token-count] = ? + , [finished] = ? + , [public-key] = ? + WHERE [number] = ? + """, + ( + voucher_state, + len(unblinded_tokens), + self.now(), + public_key, + voucher, + ), + ) + if cursor.rowcount == 0: + raise ValueError("Cannot insert tokens for unknown voucher; add voucher first") cursor.executemany( """ INSERT INTO [unblinded-tokens] VALUES (?) @@ -406,16 +346,6 @@ class VoucherStore(object): in unblinded_tokens ), ) - cursor.execute( - """ - UPDATE [vouchers] - SET [state] = "redeemed" - , [token-count] = ? - , [finished] = ? - WHERE [number] = ? - """, - (len(unblinded_tokens), self.now(), voucher), - ) @with_cursor def mark_voucher_double_spent(self, cursor, voucher): @@ -770,8 +700,20 @@ class Redeeming(object): @attr.s(frozen=True) class Redeemed(object): + """ + The voucher was successfully redeemed. Associated tokens were retrieved + and stored locally. + + :ivar datetime finished: The time when the redemption finished. + + :ivar int token_count: The number of tokens the voucher was redeemed for. + + :ivar unicode public_key: The public part of the key used to sign the + tokens for this voucher. + """ finished = attr.ib(validator=attr.validators.instance_of(datetime)) token_count = attr.ib(validator=attr.validators.instance_of((int, long))) + public_key = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(unicode))) def should_start_redemption(self): return False @@ -781,6 +723,7 @@ class Redeemed(object): u"name": u"redeemed", u"finished": self.finished.isoformat(), u"token-count": self.token_count, + u"public-key": self.public_key, } @@ -847,12 +790,9 @@ class Voucher(object): :ivar datetime created: The time at which this voucher was added to this node. - :ivar bool redeemed: ``True`` if this voucher has successfully been - redeemed with a payment server, ``False`` otherwise. - - :ivar int token_count: A number of tokens received from the redemption of - this voucher if it has been redeemed, ``None`` if it has not been - redeemed. + :ivar state: An indication of the current state of this voucher. This is + an instance of ``Pending``, ``Redeeming``, ``Redeemed``, + ``DoubleSpend``, ``Unpaid``, or ``Error``. """ number = attr.ib( validator=attr.validators.and_( @@ -880,6 +820,7 @@ class Voucher(object): return Redeemed( parse_datetime(row[0], delimiter=u" "), row[1], + row[2], ) raise ValueError("Unknown voucher state {}".format(state)) @@ -920,6 +861,7 @@ class Voucher(object): state = Redeemed( finished=parse_datetime(state_json[u"finished"]), token_count=state_json[u"token-count"], + public_key=state_json[u"public-key"], ) elif state_name == u"unpaid": state = Unpaid( diff --git a/src/_zkapauthorizer/schema.py b/src/_zkapauthorizer/schema.py new file mode 100644 index 0000000000000000000000000000000000000000..fc078c2a102f75decc5a21db4592468bf5e3f1e7 --- /dev/null +++ b/src/_zkapauthorizer/schema.py @@ -0,0 +1,134 @@ +# 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. + +from __future__ import ( + unicode_literals, +) + +""" +This module defines the database schema used by the model interface. +""" + +def get_schema_version(cursor): + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS [version] AS SELECT 0 AS [version] + """ + ) + cursor.execute( + """ + SELECT [version] FROM [version] + """ + ) + [(actual_version,)] = cursor.fetchall() + return actual_version + + +def get_schema_upgrades(from_version): + """ + Generate unicode strings containing SQL expressions to alter a schema from + ``from_version`` to the latest version. + + :param int from_version: The version of the schema which may require + upgrade. + """ + while from_version in _UPGRADES: + for upgrade in _UPGRADES[from_version]: + yield upgrade + yield _INCREMENT_VERSION + from_version += 1 + + +def run_schema_upgrades(upgrades, cursor): + """ + Apply the given upgrades using the given cursor. + + :param list[unicode] upgrades: The SQL statements to apply for the + upgrade. + + :param cursor: A DB-API cursor to use to run the SQL. + """ + for upgrade in upgrades: + cursor.execute(upgrade) + + +_INCREMENT_VERSION = ( + """ + UPDATE [version] + SET [version] = [version] + 1 + """ +) + +# A mapping from old schema versions to lists of unicode strings of SQL to +# execute against that version of the schema to create the successor schema. +_UPGRADES = { + 0: [ + """ + CREATE TABLE [vouchers] ( + [number] text, + [created] text, -- An ISO8601 date+time string. + [state] text DEFAULT "pending", -- pending, double-spend, redeemed + + [finished] text DEFAULT NULL, -- ISO8601 date+time string when + -- the current terminal state was entered. + + [token-count] num DEFAULT NULL, -- Set in the redeemed state to the number + -- of tokens received on this voucher's + -- redemption. + + PRIMARY KEY([number]) + ) + """, + """ + CREATE TABLE [tokens] ( + [text] text, -- The random string that defines the token. + [voucher] text, -- Reference to the voucher these tokens go with. + + PRIMARY KEY([text]) + FOREIGN KEY([voucher]) REFERENCES [vouchers]([number]) + ) + """, + """ + CREATE TABLE [unblinded-tokens] ( + [token] text, -- The base64 encoded unblinded token. + + PRIMARY KEY([token]) + ) + """, + """ + CREATE TABLE [lease-maintenance-spending] ( + [id] integer, -- A unique identifier for a group of activity. + [started] text, -- ISO8601 date+time string when the activity began. + [finished] text, -- ISO8601 date+time string when the activity completed (or null). + + -- The number of passes that would be required to renew all + -- shares encountered during this activity. Note that because + -- leases on different shares don't necessarily expire at the + -- same time this is not necessarily the number of passes + -- **actually** used during this activity. Some shares may + -- not have required lease renewal. Also note that while the + -- activity is ongoing this value may change. + [count] integer, + + PRIMARY KEY([id]) + ) + """, + ], + + 1: [ + """ + ALTER TABLE [vouchers] ADD COLUMN [public-key] text + """, + ], +} diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py index 0da7adf84af23d599a7ef8f4556d9d5a28bd2c10..51861424fd41d186d78f2de5539c82dd6b8765eb 100644 --- a/src/_zkapauthorizer/tests/strategies.py +++ b/src/_zkapauthorizer/tests/strategies.py @@ -75,6 +75,8 @@ from ..model import ( Voucher, Pending, DoubleSpend, + Unpaid, + Error, Redeemed, ) @@ -185,6 +187,24 @@ def node_nicknames(): ) +def dummy_ristretto_keys(): + """ + Build string values which one could imagine might be Ristretto-flavored + PrivacyPass signing or public keys. + + They're not really because they're entirely random rather than points on + the curve. + """ + return binary( + min_size=32, + max_size=32, + ).map( + b64encode, + ).map( + lambda bs: bs.decode("ascii"), + ) + + def server_configurations(signing_key_path): """ Build configuration values for the server-side plugin. @@ -277,6 +297,16 @@ def vouchers(): lambda voucher: voucher.decode("ascii"), ) +def redeemed_states(): + """ + Build ``Redeemed`` instances. + """ + return builds( + Redeemed, + finished=datetimes(), + token_count=one_of(integers(min_value=1)), + public_key=dummy_ristretto_keys(), + ) def voucher_states(): """ @@ -284,19 +314,24 @@ def voucher_states(): """ return one_of( just(Pending()), + redeemed_states(), builds( DoubleSpend, finished=datetimes(), ), builds( - Redeemed, + Unpaid, + finished=datetimes(), + ), + builds( + Error, finished=datetimes(), - token_count=one_of(integers(min_value=1)), + details=text(), ), ) -def voucher_objects(): +def voucher_objects(states=voucher_states()): """ Build ``Voucher`` instances. """ @@ -304,7 +339,7 @@ def voucher_objects(): Voucher, number=vouchers(), created=one_of(none(), datetimes()), - state=voucher_states(), + state=states, ) diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py index f901d020fa1e23d9b26c141369996560d00a0d44..423f4950de7cadac563576da99496f0c47f18c4b 100644 --- a/src/_zkapauthorizer/tests/test_client_resource.py +++ b/src/_zkapauthorizer/tests/test_client_resource.py @@ -768,6 +768,7 @@ class VoucherTests(TestCase): state=Equals(Redeemed( finished=now, token_count=NUM_TOKENS, + public_key=None, )), ), ) @@ -913,6 +914,7 @@ class VoucherTests(TestCase): state=Redeemed( finished=now, token_count=NUM_TOKENS, + public_key=None, ), ).marshal() for voucher diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py index 37997eecd565be664e62887ed89244aec5f1bae7..1c1af180ec01737e5e699da40f4575047273de27 100644 --- a/src/_zkapauthorizer/tests/test_controller.py +++ b/src/_zkapauthorizer/tests/test_controller.py @@ -116,6 +116,7 @@ from .strategies import ( tahoe_configs, vouchers, voucher_objects, + dummy_ristretto_keys, clocks, ) from .matchers import ( @@ -149,15 +150,15 @@ class PaymentControllerTests(TestCase): Equals(model_Pending()), ) - @given(tahoe_configs(), datetimes(), vouchers()) - def test_redeemed_after_redeeming(self, get_config, now, voucher): + @given(tahoe_configs(), dummy_ristretto_keys(), datetimes(), vouchers()) + def test_redeemed_after_redeeming(self, get_config, public_key, now, voucher): """ A ``Voucher`` is marked as redeemed after ``IRedeemer.redeem`` succeeds. """ store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store controller = PaymentController( store, - DummyRedeemer(), + DummyRedeemer(public_key), default_token_count=10, ) controller.redeem(voucher) @@ -168,6 +169,7 @@ class PaymentControllerTests(TestCase): Equals(model_Redeemed( finished=now, token_count=10, + public_key=public_key, )), ) @@ -319,11 +321,16 @@ class RistrettoRedeemerTests(TestCase): self.assertThat( d, succeeded( - MatchesAll( - AllMatch( - IsInstance(UnblindedToken), + MatchesStructure( + unblinded_tokens=MatchesAll( + AllMatch( + IsInstance(UnblindedToken), + ), + HasLength(num_tokens), + ), + public_key=Equals( + PublicKey.from_signing_key(signing_key).encode_base64(), ), - HasLength(num_tokens), ), ), ) @@ -431,8 +438,8 @@ class RistrettoRedeemerTests(TestCase): voucher, random_tokens, ) - def unblinded_tokens_to_passes(unblinded_tokens): - passes = redeemer.tokens_to_passes(message, unblinded_tokens) + def unblinded_tokens_to_passes(result): + passes = redeemer.tokens_to_passes(message, result.unblinded_tokens) return passes d.addCallback(unblinded_tokens_to_passes) diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py index 00dc6eca2395628b38bc56c1d1a7ef7a20ddb321..c4885ed96367e4227a46dc080e2f66df1895d780 100644 --- a/src/_zkapauthorizer/tests/test_model.py +++ b/src/_zkapauthorizer/tests/test_model.py @@ -68,16 +68,12 @@ from hypothesis.strategies import ( from twisted.python.runtime import ( platform, ) -from twisted.python.filepath import ( - FilePath, -) from ..storage_common import ( BYTES_PER_PASS, ) from ..model import ( - SchemaError, StoreOpenError, VoucherStore, Voucher, @@ -85,7 +81,6 @@ from ..model import ( DoubleSpend, Redeemed, LeaseMaintenanceActivity, - open_and_initialize, memory_connect, ) @@ -96,6 +91,7 @@ from .strategies import ( random_tokens, unblinded_tokens, posix_safe_datetimes, + dummy_ristretto_keys, ) from .fixtures import ( TemporaryVoucherStore, @@ -106,22 +102,6 @@ class VoucherStoreTests(TestCase): """ Tests for ``VoucherStore``. """ - def test_create_mismatched_schema(self): - """ - ``open_and_initialize`` raises ``SchemaError`` if asked for a database - with a schema version other than it can create. - """ - tempdir = self.useFixture(TempDir()) - dbpath = tempdir.join(b"db.sqlite3") - self.assertThat( - lambda: open_and_initialize( - FilePath(dbpath), - required_schema_version=100, - ), - raises(SchemaError), - ) - - @given(tahoe_configs(), datetimes(), vouchers()) def test_get_missing(self, get_config, now, voucher): """ @@ -348,19 +328,71 @@ class VoucherTests(TestCase): ) +def paired_tokens(data): + """ + Draw two lists of the same length, one of random tokens and one of + unblinded tokens. + + :rtype: ([RandomTokens], [UnblindedTokens]) + """ + num_tokens = data.draw(integers(min_value=1, max_value=1000)) + r = data.draw(lists( + random_tokens(), + min_size=num_tokens, + max_size=num_tokens, + unique=True, + )) + u = data.draw(lists( + unblinded_tokens(), + min_size=num_tokens, + max_size=num_tokens, + unique=True, + )) + return r, u + + class UnblindedTokenStoreTests(TestCase): """ Tests for ``UnblindedToken``-related functionality of ``VoucherStore``. """ - @given(tahoe_configs(), datetimes(), vouchers(), lists(unblinded_tokens(), unique=True)) - def test_unblinded_tokens_round_trip(self, get_config, now, voucher_value, tokens): + @given( + tahoe_configs(), + datetimes(), + vouchers(), + dummy_ristretto_keys(), + lists(unblinded_tokens(), unique=True), + ) + def test_unblinded_tokens_without_voucher(self, get_config, now, voucher_value, public_key, unblinded_tokens): + """ + Unblinded tokens for a voucher which has not been added to the store cannot be inserted. + """ + store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store + try: + result = store.insert_unblinded_tokens_for_voucher(voucher_value, public_key, unblinded_tokens) + except ValueError: + pass + except Exception as e: + self.fail("insert_unblinded_tokens_for_voucher raised the wrong exception: {}".format(e)) + else: + self.fail("insert_unblinded_tokens_for_voucher didn't raise, returned: {}".format(result)) + + @given( + tahoe_configs(), + datetimes(), + vouchers(), + dummy_ristretto_keys(), + data(), + ) + def test_unblinded_tokens_round_trip(self, get_config, now, voucher_value, public_key, data): """ Unblinded tokens that are added to the store can later be retrieved. """ + random_tokens, unblinded_tokens = paired_tokens(data) store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store - store.insert_unblinded_tokens_for_voucher(voucher_value, tokens) - retrieved_tokens = store.extract_unblinded_tokens(len(tokens)) - self.expectThat(tokens, AfterPreprocessing(sorted, Equals(retrieved_tokens))) + store.add(voucher_value, lambda: random_tokens) + store.insert_unblinded_tokens_for_voucher(voucher_value, public_key, unblinded_tokens) + retrieved_tokens = store.extract_unblinded_tokens(len(random_tokens)) + self.expectThat(unblinded_tokens, AfterPreprocessing(sorted, Equals(retrieved_tokens))) # After extraction, the unblinded tokens are no longer available. more_unblinded_tokens = store.extract_unblinded_tokens(1) @@ -370,10 +402,11 @@ class UnblindedTokenStoreTests(TestCase): tahoe_configs(), datetimes(), vouchers(), + dummy_ristretto_keys(), integers(min_value=1, max_value=100), data(), ) - def test_mark_vouchers_redeemed(self, get_config, now, voucher_value, num_tokens, data): + def test_mark_vouchers_redeemed(self, get_config, now, voucher_value, public_key, num_tokens, data): """ The voucher for unblinded tokens that are added to the store is marked as redeemed. @@ -397,7 +430,7 @@ class UnblindedTokenStoreTests(TestCase): store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store store.add(voucher_value, lambda: random) - store.insert_unblinded_tokens_for_voucher(voucher_value, unblinded) + store.insert_unblinded_tokens_for_voucher(voucher_value, public_key, unblinded) loaded_voucher = store.get(voucher_value) self.assertThat( loaded_voucher, @@ -405,6 +438,7 @@ class UnblindedTokenStoreTests(TestCase): state=Equals(Redeemed( finished=now, token_count=num_tokens, + public_key=public_key, )), ), ) @@ -437,10 +471,11 @@ class UnblindedTokenStoreTests(TestCase): tahoe_configs(), datetimes(), vouchers(), + dummy_ristretto_keys(), integers(min_value=1, max_value=100), data(), ) - def test_mark_spent_vouchers_double_spent(self, get_config, now, voucher_value, num_tokens, data): + def test_mark_spent_vouchers_double_spent(self, get_config, now, voucher_value, public_key, num_tokens, data): """ A voucher which has already been spent cannot be marked as double-spent. """ @@ -462,7 +497,7 @@ class UnblindedTokenStoreTests(TestCase): ) store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store store.add(voucher_value, lambda: random) - store.insert_unblinded_tokens_for_voucher(voucher_value, unblinded) + store.insert_unblinded_tokens_for_voucher(voucher_value, public_key, unblinded) try: result = store.mark_voucher_double_spent(voucher_value) except ValueError: diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py index a8fff0d78b28dd5a6adcde78467d2b9bdb1178a8..6fb6a13963c7f4ca92ae36f4e0d91266da5f91ab 100644 --- a/src/_zkapauthorizer/tests/test_plugin.py +++ b/src/_zkapauthorizer/tests/test_plugin.py @@ -104,6 +104,8 @@ from ..model import ( ) from ..controller import ( IssuerConfigurationMismatch, + PaymentController, + DummyRedeemer, ) from .._storage_client import ( IncorrectStorageServerReference, @@ -120,8 +122,6 @@ from .strategies import ( server_configurations, announcements, vouchers, - random_tokens, - unblinded_tokens, storage_indexes, lease_renew_secrets, lease_cancel_secrets, @@ -386,8 +386,6 @@ class ClientPluginTests(TestCase): datetimes(), announcements(), vouchers(), - random_tokens(), - unblinded_tokens(), storage_indexes(), lease_renew_secrets(), lease_cancel_secrets(), @@ -400,8 +398,6 @@ class ClientPluginTests(TestCase): now, announcement, voucher, - token, - unblinded_token, storage_index, renew_secret, cancel_secret, @@ -419,8 +415,17 @@ class ClientPluginTests(TestCase): ) store = VoucherStore.from_node_config(node_config, lambda: now) - store.add(voucher, lambda: [token]) - store.insert_unblinded_tokens_for_voucher(voucher, [unblinded_token]) + controller = PaymentController( + store, + DummyRedeemer(), + 1, + ) + # Get a token inserted into the store. + redeeming = controller.redeem(voucher) + self.assertThat( + redeeming, + succeeded(Always()), + ) storage_client = storage_server.get_storage_client( node_config, diff --git a/src/_zkapauthorizer/tests/test_schema.py b/src/_zkapauthorizer/tests/test_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..da1af22218d7431852e341f6e7a7d1106d65f6d1 --- /dev/null +++ b/src/_zkapauthorizer/tests/test_schema.py @@ -0,0 +1,43 @@ +# coding: utf-8 +# Copyright 2019 PrivateStorage.io, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Tests for ``_zkapauthorizer.schema``. +""" + +from __future__ import ( + absolute_import, +) + +from testtools import ( + TestCase, +) +from testtools.matchers import ( + Equals, +) + +from ..schema import ( + _UPGRADES, +) + +class UpgradeTests(TestCase): + def test_consistency(self): + """ + Upgrades are defined for every version up to the latest version. + """ + self.assertThat( + list(_UPGRADES.keys()), + Equals(list(range(len(_UPGRADES)))), + )