From 7bffec04b19dacacefeb3eadfd13c0d2ae2756fd Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone <exarkun@twistedmatrix.com> Date: Fri, 15 Nov 2019 14:10:20 -0500 Subject: [PATCH] Normalize the representation of voucher state Not in the database but in the Python and JSON representations --- src/_zkapauthorizer/model.py | 101 +++++++++++++++--- src/_zkapauthorizer/tests/strategies.py | 17 ++- .../tests/test_client_resource.py | 34 +++--- src/_zkapauthorizer/tests/test_controller.py | 14 ++- src/_zkapauthorizer/tests/test_model.py | 18 ++-- 5 files changed, 141 insertions(+), 43 deletions(-) diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py index ba9089e..c2a6f79 100644 --- a/src/_zkapauthorizer/model.py +++ b/src/_zkapauthorizer/model.py @@ -126,7 +126,10 @@ def open_and_initialize(path, required_schema_version, connect=None): [created] text, -- An ISO8601 date+time string. [state] text DEFAULT "pending", -- pending, double-spend, redeemed - [token-count] num DEFAULT NULL, -- Set in the redeemed state to the number + [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. @@ -224,7 +227,7 @@ class VoucherStore(object): cursor.execute( """ SELECT - [number], [created], [state], [token-count] + [number], [created], [state], [finished], [token-count] FROM [vouchers] WHERE @@ -282,7 +285,10 @@ class VoucherStore(object): """ cursor.execute( """ - SELECT [number], [created], [state], [token-count] FROM [vouchers] + SELECT + [number], [created], [state], [finished], [token-count] + FROM + [vouchers] """, ) refs = cursor.fetchall() @@ -320,9 +326,10 @@ class VoucherStore(object): UPDATE [vouchers] SET [state] = "redeemed" , [token-count] = ? + , [finished] = ? WHERE [number] = ? """, - (len(unblinded_tokens), voucher), + (len(unblinded_tokens), self.now(), voucher), ) @with_cursor @@ -335,10 +342,11 @@ class VoucherStore(object): """ UPDATE [vouchers] SET [state] = "double-spend" + , [finished] = ? WHERE [number] = ? AND [state] = "pending" """, - (voucher,) + (self.now(), voucher), ) if cursor.rowcount == 0: # Was there no matching voucher or was it in the wrong state? @@ -456,6 +464,22 @@ class RandomToken(object): token_value = attr.ib(validator=attr.validators.instance_of(unicode)) +@attr.s(frozen=True) +class Pending(object): + pass + + +@attr.s(frozen=True) +class Redeemed(object): + finished = attr.ib(validator=attr.validators.instance_of(datetime)) + token_count = attr.ib(validator=attr.validators.instance_of((int, long))) + + +@attr.s(frozen=True) +class DoubleSpend(object): + finished = attr.ib(validator=attr.validators.instance_of(datetime)) + + @attr.s class Voucher(object): """ @@ -473,22 +497,38 @@ class Voucher(object): redeemed. """ number = attr.ib() - created = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(datetime))) - state = attr.ib(default=u"pending", validator=attr.validators.in_((u"pending", u"double-spend", u"redeemed"))) - token_count = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of((int, long)))) + created = attr.ib( + default=None, + validator=attr.validators.optional(attr.validators.instance_of(datetime)), + ) + state = attr.ib(default=Pending()) @classmethod def from_row(cls, row): + def state_from_row(state, row): + if state == u"pending": + return Pending() + if state == u"double-spend": + return DoubleSpend( + parse_datetime(row[0], delimiter=u" "), + ) + if state == u"redeemed": + return Redeemed( + parse_datetime(row[0], delimiter=u" "), + row[1], + ) + raise ValueError("Unknown voucher state {}".format(state)) + + number, created, state = row[:3] return cls( - row[0], + number, # All Python datetime-based date/time libraries fail to handle # leap seconds. This parse call might raise an exception of the # 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. - parse_datetime(row[1], delimiter=u" "), - row[2], - row[3], + parse_datetime(created, delimiter=u" "), + state_from_row(state, row[3:]) ) @classmethod @@ -500,11 +540,26 @@ class Voucher(object): @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() + elif state_name == u"double-spend": + state = DoubleSpend( + finished=parse_datetime(state_json[u"finished"]), + ) + elif state_name == u"redeemed": + state = Redeemed( + finished=parse_datetime(state_json[u"finished"]), + token_count=state_json[u"token-count"], + ) + else: + raise ValueError("Unrecognized state {}".format(state_json)) + return cls( number=values[u"number"], created=None if values[u"created"] is None else parse_datetime(values[u"created"]), - state=values[u"state"], - token_count=values[u"token-count"], + state=state, ) @@ -517,10 +572,24 @@ class Voucher(object): def to_json_v1(self): + if isinstance(self.state, Pending): + state = { + u"name": u"pending", + } + elif isinstance(self.state, DoubleSpend): + state = { + u"name": u"double-spend", + u"finished": self.state.finished.isoformat(), + } + elif isinstance(self.state, Redeemed): + state = { + u"name": u"redeemed", + u"finished": self.state.finished.isoformat(), + u"token-count": self.state.token_count, + } return { u"number": self.number, u"created": None if self.created is None else self.created.isoformat(), - u"state": self.state, - u"token-count": self.token_count, + u"state": state, u"version": 1, } diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py index 47c1c8f..d990256 100644 --- a/src/_zkapauthorizer/tests/strategies.py +++ b/src/_zkapauthorizer/tests/strategies.py @@ -56,6 +56,9 @@ from ..model import ( RandomToken, UnblindedToken, Voucher, + Pending, + DoubleSpend, + Redeemed, ) @@ -233,9 +236,16 @@ def voucher_states(): Build unicode strings giving states a Voucher can be in. """ return one_of( - just(u"pending"), - just(u"double-spend"), - just(u"redeemed"), + just(Pending()), + builds( + DoubleSpend, + finished=datetimes(), + ), + builds( + Redeemed, + finished=datetimes(), + token_count=one_of(integers(min_value=1)), + ), ) @@ -248,7 +258,6 @@ def voucher_objects(): number=vouchers(), created=one_of(none(), datetimes()), state=voucher_states(), - token_count=one_of(none(), integers(min_value=1)), ) diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py index fdfeed0..c63961a 100644 --- a/src/_zkapauthorizer/tests/test_client_resource.py +++ b/src/_zkapauthorizer/tests/test_client_resource.py @@ -53,7 +53,6 @@ from testtools.matchers import ( IsInstance, ContainsDict, AfterPreprocessing, - Is, Equals, Always, GreaterThan, @@ -113,6 +112,9 @@ from treq.testing import ( from ..model import ( Voucher, + Pending, + Redeemed, + DoubleSpend, VoucherStore, memory_connect, ) @@ -609,8 +611,7 @@ class VoucherTests(TestCase): MatchesStructure( number=Equals(voucher), created=Equals(now), - state=Equals(u"pending"), - token_count=Is(None), + state=Equals(Pending()), ), ) @@ -629,10 +630,12 @@ class VoucherTests(TestCase): MatchesStructure( number=Equals(voucher), created=Equals(now), - state=Equals(u"redeemed"), - # Value duplicated from PaymentController.redeem default. - # Should do this better. - token_count=Equals(100), + state=Equals(Redeemed( + finished=now, + # Value duplicated from PaymentController.redeem default. + # Should do this better. + token_count=100, + )), ), ) @@ -652,8 +655,9 @@ class VoucherTests(TestCase): MatchesStructure( number=Equals(voucher), created=Equals(now), - state=Equals(u"double-spend"), - token_count=Is(None), + state=Equals(DoubleSpend( + finished=now, + )), ), ) @@ -765,11 +769,13 @@ class VoucherTests(TestCase): Voucher( voucher, created=now, - state=u"redeemed", - # Value duplicated from - # PaymentController.redeem default. - # Should do this better. - token_count=100, + state=Redeemed( + finished=now, + # Value duplicated from + # PaymentController.redeem + # default. Should do this better. + token_count=100, + ), ).marshal() for voucher in vouchers diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py index c357ca4..31be5bf 100644 --- a/src/_zkapauthorizer/tests/test_controller.py +++ b/src/_zkapauthorizer/tests/test_controller.py @@ -106,6 +106,9 @@ from ..model import ( memory_connect, VoucherStore, UnblindedToken, + Pending, + DoubleSpend, + Redeemed, ) from .strategies import ( @@ -145,7 +148,7 @@ class PaymentControllerTests(TestCase): persisted_voucher = store.get(voucher) self.assertThat( persisted_voucher.state, - Equals(u"pending"), + Equals(Pending()), ) @given(tahoe_configs(), datetimes(), vouchers()) @@ -171,7 +174,10 @@ class PaymentControllerTests(TestCase): persisted_voucher = store.get(voucher) self.assertThat( persisted_voucher.state, - Equals(u"redeemed"), + Equals(Redeemed( + finished=now, + token_count=100, + )), ) @given(tahoe_configs(), datetimes(), vouchers()) @@ -199,7 +205,9 @@ class PaymentControllerTests(TestCase): self.assertThat( persisted_voucher, MatchesStructure( - state=Equals(u"double-spend"), + state=Equals(DoubleSpend( + finished=now, + )), ), ) diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py index 2abed45..144999c 100644 --- a/src/_zkapauthorizer/tests/test_model.py +++ b/src/_zkapauthorizer/tests/test_model.py @@ -65,6 +65,9 @@ from ..model import ( StoreOpenError, VoucherStore, Voucher, + Pending, + DoubleSpend, + Redeemed, open_and_initialize, memory_connect, ) @@ -122,7 +125,7 @@ class VoucherStoreTests(TestCase): store.get(voucher), MatchesStructure( number=Equals(voucher), - state=Equals(u"pending"), + state=Equals(Pending()), created=Equals(now), ), ) @@ -141,8 +144,7 @@ class VoucherStoreTests(TestCase): MatchesStructure( number=Equals(voucher), created=Equals(now), - state=Equals(u"pending"), - token_count=Equals(None), + state=Equals(Pending()), ), ) @@ -302,8 +304,10 @@ class UnblindedTokenStoreTests(TestCase): self.assertThat( loaded_voucher, MatchesStructure( - state=Equals(u"redeemed"), - token_count=Equals(num_tokens), + state=Equals(Redeemed( + finished=now, + token_count=num_tokens, + )), ), ) @@ -325,7 +329,9 @@ class UnblindedTokenStoreTests(TestCase): self.assertThat( voucher, MatchesStructure( - state=Equals(u"double-spend"), + state=Equals(DoubleSpend( + finished=now, + )), ), ) -- GitLab