diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py index 8615b43955dd64b54a7ea0b2827bb283f6fc02e7..f8d69c2ca21b2cc6061f2d86673e30a1bd2a68be 100644 --- a/src/_zkapauthorizer/model.py +++ b/src/_zkapauthorizer/model.py @@ -211,7 +211,7 @@ class VoucherStore(object): cursor.execute( """ SELECT - [number], [created], [state], [finished], [token-count], [public-key] + [number], [created], [state], [finished], [token-count], [public-key], [counter] FROM [vouchers] WHERE @@ -295,7 +295,7 @@ class VoucherStore(object): cursor.execute( """ SELECT - [number], [created], [state], [finished], [token-count], [public-key] + [number], [created], [state], [finished], [token-count], [public-key], [counter] FROM [vouchers] """, @@ -637,6 +637,20 @@ def has_length(expected): ) return validate_has_length +def greater_than(expected): + def validate_relation(inst, attr, value): + if value > expected: + return None + + raise ValueError( + "{name!r} must be greater than {expected}, instead it was {actual}".format( + name=attr.name, + expected=expected, + actual=value, + ), + ) + return validate_relation + @attr.s(frozen=True) class UnblindedToken(object): @@ -710,12 +724,26 @@ class RandomToken(object): @attr.s(frozen=True) class Pending(object): + """ + The voucher has not yet been completely redeemed for ZKAPs. + + :ivar int counter: The number of partial redemptions which have been + successfully performed for the voucher. + """ + counter = attr.ib( + validator=attr.validators.and_( + attr.validators.instance_of((int, long)), + greater_than(-1), + ), + ) + def should_start_redemption(self): return True def to_json_v1(self): return { u"name": u"pending", + u"counter": self.counter, } @@ -845,13 +873,15 @@ class Voucher(object): default=None, validator=attr.validators.optional(attr.validators.instance_of(datetime)), ) - state = attr.ib(default=Pending()) + state = attr.ib(default=Pending(counter=0)) @classmethod def from_row(cls, row): def state_from_row(state, row): if state == u"pending": - return Pending() + # TODO: The 0 here should be row[3] but I can't write a test + # to prove it yet. + return Pending(counter=0) if state == u"double-spend": return DoubleSpend( parse_datetime(row[0], delimiter=u" "), @@ -888,7 +918,7 @@ class Voucher(object): state_json = values[u"state"] state_name = state_json[u"name"] if state_name == u"pending": - state = Pending() + state = Pending(counter=state_json[u"counter"]) elif state_name == u"redeeming": state = Redeeming( started=parse_datetime(state_json[u"started"]), diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py index b648c5c5778e9d7d3d44572ecf5f05657e85c228..7776ff739b3ec50b88b37f8ecf1a307a7eba1318 100644 --- a/src/_zkapauthorizer/tests/strategies.py +++ b/src/_zkapauthorizer/tests/strategies.py @@ -310,10 +310,13 @@ def redeemed_states(): def voucher_states(): """ - Build unicode strings giving states a Voucher can be in. + Build Python objects representing states a Voucher can be in. """ return one_of( - just(Pending()), + builds( + Pending, + integers(min_value=0), + ), redeemed_states(), builds( DoubleSpend, diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py index 608ef72679232b29a3681b224d2eb61f98f2fe2e..16d12554bfc9aececbf627543c14c8102c2e1923 100644 --- a/src/_zkapauthorizer/tests/test_controller.py +++ b/src/_zkapauthorizer/tests/test_controller.py @@ -153,7 +153,7 @@ class PaymentControllerTests(TestCase): persisted_voucher = store.get(voucher) self.assertThat( persisted_voucher.state, - Equals(model_Pending()), + Equals(model_Pending(counter=0)), ) @given(tahoe_configs(), dummy_ristretto_keys(), datetimes(), vouchers()) diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py index 27fbcab25f7d9597c28ee61372c080c86df6cf07..e6df76c1ad9921552af6abb0667f341d76f7c57c 100644 --- a/src/_zkapauthorizer/tests/test_model.py +++ b/src/_zkapauthorizer/tests/test_model.py @@ -129,7 +129,7 @@ class VoucherStoreTests(TestCase): store.get(voucher), MatchesStructure( number=Equals(voucher), - state=Equals(Pending()), + state=Equals(Pending(counter=0)), created=Equals(now), ), ) @@ -148,7 +148,7 @@ class VoucherStoreTests(TestCase): MatchesStructure( number=Equals(voucher), created=Equals(now), - state=Equals(Pending()), + state=Equals(Pending(counter=0)), ), ) self.assertThat(