diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py index 8691fface49804e256e2a003930cb9035c155da7..3cb8204f300cc894b3d40ff1d80aed4b0d0e2d0b 100644 --- a/src/_zkapauthorizer/model.py +++ b/src/_zkapauthorizer/model.py @@ -94,6 +94,13 @@ class StoreOpenError(Exception): self.reason = reason +class NotEnoughTokens(Exception): + """ + An attempt to extract tokens failed because the store does not contain as + many tokens as were requested. + """ + + CONFIG_DB_NAME = u"privatestorageio-zkapauthz-v1.sqlite3" def open_and_initialize(path, connect=None): @@ -396,6 +403,16 @@ class VoucherStore(object): :return list[UnblindedTokens]: The removed unblinded tokens. """ + cursor.execute( + """ + SELECT COUNT(token) + FROM [unblinded-tokens] + """, + ) + [(existing_tokens,)] = cursor.fetchall() + if existing_tokens < count: + raise NotEnoughTokens() + cursor.execute( """ CREATE TEMPORARY TABLE [extracting] diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py index c4885ed96367e4227a46dc080e2f66df1895d780..5975f597da9be24bcbd9d9f8fcf153daa464cfb1 100644 --- a/src/_zkapauthorizer/tests/test_model.py +++ b/src/_zkapauthorizer/tests/test_model.py @@ -75,6 +75,7 @@ from ..storage_common import ( from ..model import ( StoreOpenError, + NotEnoughTokens, VoucherStore, Voucher, Pending, @@ -395,8 +396,14 @@ class UnblindedTokenStoreTests(TestCase): 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) - self.expectThat([], Equals(more_unblinded_tokens)) + try: + result = store.extract_unblinded_tokens(1) + except NotEnoughTokens: + pass + except Exception as e: + self.fail("extract_unblinded_tokens raised wrong exception: {}".format(e)) + else: + self.fail("extract_unblinded_tokens didn't raise, returned: {}".format(result)) @given( tahoe_configs(), @@ -526,6 +533,49 @@ class UnblindedTokenStoreTests(TestCase): else: self.fail("mark_voucher_double_spent didn't raise, returned: {}".format(result)) + @given( + tahoe_configs(), + datetimes(), + vouchers(), + dummy_ristretto_keys(), + integers(min_value=1, max_value=100), + integers(min_value=1), + data(), + ) + def test_not_enough_unblinded_tokens(self, get_config, now, voucher_value, public_key, num_tokens, extra, data): + """ + ``extract_unblinded_tokens`` raises ``NotEnoughTokens`` if ``count`` is + greater than the number of unblinded tokens in the store. + """ + random = data.draw( + lists( + random_tokens(), + min_size=num_tokens, + max_size=num_tokens, + unique=True, + ), + ) + unblinded = data.draw( + lists( + unblinded_tokens(), + min_size=num_tokens, + max_size=num_tokens, + unique=True, + ), + ) + store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store + store.add(voucher_value, lambda: random) + store.insert_unblinded_tokens_for_voucher(voucher_value, public_key, unblinded) + + try: + result = store.extract_unblinded_tokens(num_tokens + extra) + except NotEnoughTokens: + pass + except Exception as e: + self.fail("extract_unblinded_tokens raised wrong exception: {}".format(e)) + else: + self.fail("extract_unblinded_tokens didn't raise, returned: {}".format(result)) + # TODO: Other error states and transient states diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py index 6fb6a13963c7f4ca92ae36f4e0d91266da5f91ab..d98408cd622c40a927d36ad7e18ddd5a6e7750e5 100644 --- a/src/_zkapauthorizer/tests/test_plugin.py +++ b/src/_zkapauthorizer/tests/test_plugin.py @@ -42,7 +42,6 @@ from testtools.matchers import ( Always, Contains, AfterPreprocessing, - Equals, ) from testtools.twistedsupport import ( succeeded, @@ -100,6 +99,7 @@ from ..foolscap import ( RIPrivacyPassAuthorizedStorageServer, ) from ..model import ( + NotEnoughTokens, VoucherStore, ) from ..controller import ( @@ -107,6 +107,10 @@ from ..controller import ( PaymentController, DummyRedeemer, ) +from ..storage_common import ( + BYTES_PER_PASS, + required_passes, +) from .._storage_client import ( IncorrectStorageServerReference, ) @@ -418,7 +422,8 @@ class ClientPluginTests(TestCase): controller = PaymentController( store, DummyRedeemer(), - 1, + # Give it enough for the allocate_buckets call below. + required_passes(BYTES_PER_PASS, [size] * len(sharenums)), ) # Get a token inserted into the store. redeeming = controller.redeem(voucher) @@ -445,11 +450,14 @@ class ClientPluginTests(TestCase): ) # There should be no unblinded tokens left to extract. - remaining = store.extract_unblinded_tokens(1) - self.assertThat( - remaining, - Equals([]), - ) + try: + result = store.extract_unblinded_tokens(1) + except NotEnoughTokens: + pass + except Exception as e: + self.fail("extract_unblinded_tokens raised wrong exception: {}".format(e)) + else: + self.fail("extract_unblinded_tokens didn't raise, returned: {}".format(result)) class ClientResourceTests(TestCase):