Newer
Older
# 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 datetime import datetime, timedelta
from errno import EACCES
from os import mkdir
from unittest import skipIf
from fixtures import TempDir
from hypothesis import assume, given, note
from hypothesis.stateful import (
RuleBasedStateMachine,
invariant,
from hypothesis.strategies import (
booleans,
randoms,
from testtools import TestCase
from testtools.matchers import (
AfterPreprocessing,
Always,
Equals,
HasLength,
IsInstance,
MatchesAll,
MatchesStructure,
Raises,
Jean-Paul Calderone
committed
)
from testtools.twistedsupport import succeeded
from twisted.python.runtime import platform
from ..model import (
NotEnoughTokens,
Pending,
Redeemed,
from .fixtures import ConfiglessMemoryVoucherStore, TemporaryVoucherStore
from .matchers import raises
from .strategies import (
dummy_ristretto_keys,
pass_counts,
posix_safe_datetimes,
random_tokens,
tahoe_configs,
unblinded_tokens,
voucher_counters,
voucher_objects,
vouchers,
class VoucherStoreTests(TestCase):
Tests for ``VoucherStore``.
@given(tahoe_configs(), datetimes(), vouchers())
def test_get_missing(self, get_config, now, voucher):
``VoucherStore.get`` raises ``KeyError`` when called with a
voucher not previously added to the store.
store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
self.assertThat(
raises(KeyError),
)
@given(
tahoe_configs(),
vouchers(),
lists(random_tokens(), min_size=1, unique=True),
datetimes(),
)
def test_add(self, get_config, voucher, tokens, now):
``VoucherStore.get`` returns a ``Voucher`` representing a voucher
previously added to the store with ``VoucherStore.add``.
store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
store.add(voucher, len(tokens), 0, lambda: tokens)
self.assertThat(
MatchesStructure(
expected_tokens=Equals(len(tokens)),
state=Equals(Pending(counter=0)),
@given(
tahoe_configs(),
vouchers(),
lists(voucher_counters(), unique=True, min_size=2, max_size=2),
lists(random_tokens(), min_size=2, unique=True),
datetimes(),
)
def test_add_with_distinct_counters(
self, get_config, voucher, counters, tokens, now
):
"""
``VoucherStore.add`` adds new tokens to the store when passed the same
voucher but a different counter value.
"""
counter_a = counters[0]
counter_b = counters[1]
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
# the first call.
added_tokens_a = store.add(voucher, len(tokens), counter_a, lambda: tokens_a)
added_tokens_b = store.add(voucher, 0, counter_b, lambda: tokens_b)
self.assertThat(
store.get(voucher),
MatchesStructure(
number=Equals(voucher),
expected_tokens=Equals(len(tokens)),
state=Equals(Pending(counter=0)),
created=Equals(now),
),
)
self.assertThat(tokens_a, Equals(added_tokens_a))
self.assertThat(tokens_b, Equals(added_tokens_b))
@given(
tahoe_configs(),
vouchers(),
datetimes(),
lists(random_tokens(), min_size=1, unique=True),
)
def test_add_idempotent(self, get_config, voucher, now, tokens):
More than one call to ``VoucherStore.add`` with the same argument results
in the same state as a single call.
store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
first_tokens = store.add(
voucher,
expected_tokens=len(tokens),
counter=0,
get_tokens=lambda: tokens,
)
second_tokens = store.add(
voucher,
# The voucher should already exists in the store so the
# expected_tokens value supplied here is ignored.
expected_tokens=0,
counter=0,
# Likewise, no need to generate tokens here because counter value
# 0 was already added and tokens were generated then. If
# get_tokens were called here, it would be an error.
get_tokens=None,
)
self.assertThat(
MatchesStructure(
expected_tokens=Equals(len(tokens)),
state=Equals(Pending(counter=0)),
self.assertThat(
first_tokens,
Equals(tokens),
)
self.assertThat(
second_tokens,
Equals(tokens),
)
@given(tahoe_configs(), datetimes(), lists(vouchers(), unique=True), data())
def test_list(self, get_config, now, vouchers, data):
``VoucherStore.list`` returns a ``list`` containing a ``Voucher`` object
for each voucher previously added.
tokens = iter(
data.draw(
lists(
random_tokens(),
unique=True,
min_size=len(vouchers),
max_size=len(vouchers),
),
)
)
store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
store.add(
voucher,
expected_tokens=1,
counter=0,
get_tokens=lambda: [next(tokens)],
)
self.assertThat(
store.list(),
Equals(
list(
Voucher(number, expected_tokens=1, created=now)
for number in vouchers
)
),
Jean-Paul Calderone
committed
@skipIf(platform.isWindows(), "Hard to prevent directory creation on Windows")
@given(tahoe_configs(), datetimes())
def test_uncreateable_store_directory(self, get_config, now):
"""
If the underlying directory in the node configuration cannot be created
then ``VoucherStore.from_node_config`` raises ``StoreOpenError``.
"""
tempdir = self.useFixture(TempDir())
nodedir = tempdir.join(u"node")
# Create the node directory without permission to create the
# underlying directory.
mkdir(nodedir, 0o500)
config = get_config(nodedir, u"tub.port")
self.assertThat(
lambda: VoucherStore.from_node_config(
Raises(
AfterPreprocessing(
MatchesStructure(
reason=MatchesAll(
IsInstance(OSError),
MatchesStructure(
errno=Equals(EACCES),
),
),
),
),
),
),
)
@skipIf(
platform.isWindows(), "Hard to prevent database from being opened on Windows"
)
@given(tahoe_configs(), datetimes())
def test_unopenable_store(self, get_config, now):
"""
If the underlying database file cannot be opened then
``VoucherStore.from_node_config`` raises ``StoreOpenError``.
"""
tempdir = self.useFixture(TempDir())
nodedir = tempdir.join(u"node")
config = get_config(nodedir, u"tub.port")
# Create the underlying database file.
store = VoucherStore.from_node_config(config, lambda: now)
# Prevent further access to it.
store.database_path.chmod(0o000)
self.assertThat(
lambda: VoucherStore.from_node_config(
),
raises(StoreOpenError),
)
Jean-Paul Calderone
committed
@given(tahoe_configs(), vouchers(), dummy_ristretto_keys(), datetimes(), data())
def test_spend_order_equals_backup_order(
self, get_config, voucher_value, public_key, now, data
):
Jean-Paul Calderone
committed
"""
Unblinded tokens returned by ``VoucherStore.backup`` appear in the same
order as they are returned by ``VoucherStore.get_unblinded_tokens``.
Jean-Paul Calderone
committed
"""
backed_up_tokens, spent_tokens, inserted_tokens = self._spend_order_test(
Jean-Paul Calderone
committed
)
self.assertThat(
backed_up_tokens,
Equals(spent_tokens),
)
@given(tahoe_configs(), vouchers(), dummy_ristretto_keys(), datetimes(), data())
def test_spend_order_equals_insert_order(
self, get_config, voucher_value, public_key, now, data
):
Jean-Paul Calderone
committed
"""
Unblinded tokens returned by ``VoucherStore.get_unblinded_tokens``
Jean-Paul Calderone
committed
appear in the same order as they were inserted.
"""
backed_up_tokens, spent_tokens, inserted_tokens = self._spend_order_test(
Jean-Paul Calderone
committed
)
self.assertThat(
spent_tokens,
Equals(inserted_tokens),
)
def _spend_order_test(self, get_config, voucher_value, public_key, now, data):
"""
Insert, backup, and extract some tokens.
:param get_config: See ``tahoe_configs``
:param unicode voucher_value: A voucher value to associate with the tokens.
:param unicode public_key: A public key to associate with inserted unblinded tokens.
:param datetime now: A time to pretend is current.
:param data: A Hypothesis data for drawing values from strategies.
:return: A three-tuple of (backed up tokens, extracted tokens, inserted tokens).
"""
Jean-Paul Calderone
committed
tempdir = self.useFixture(TempDir())
nodedir = tempdir.join(u"node")
Jean-Paul Calderone
committed
config = get_config(nodedir, u"tub.port")
Jean-Paul Calderone
committed
# Create the underlying database file.
store = VoucherStore.from_node_config(config, lambda: now)
# Put some tokens in it that we can backup and extract
random_tokens, unblinded_tokens = paired_tokens(
data, integers(min_value=1, max_value=5)
)
store.add(voucher_value, len(random_tokens), 0, lambda: random_tokens)
Jean-Paul Calderone
committed
store.insert_unblinded_tokens_for_voucher(
voucher_value,
public_key,
unblinded_tokens,
completed=data.draw(booleans()),
spendable=True,
Jean-Paul Calderone
committed
)
backed_up_tokens = store.backup()[u"unblinded-tokens"]
extracted_tokens = []
tokens_remaining = len(unblinded_tokens)
while tokens_remaining > 0:
to_spend = data.draw(integers(min_value=1, max_value=tokens_remaining))
extracted_tokens.extend(
token.unblinded_token.decode("ascii")
for token in store.get_unblinded_tokens(to_spend)
Jean-Paul Calderone
committed
)
tokens_remaining -= to_spend
return (
backed_up_tokens,
extracted_tokens,
list(token.unblinded_token.decode("ascii") for token in unblinded_tokens),
Jean-Paul Calderone
committed
)
class UnblindedTokenStateMachine(RuleBasedStateMachine):
"""
Transition rules for a state machine corresponding to the state of
unblinded tokens in a ``VoucherStore`` - usable, in-use, spent, invalid,
etc.
"""
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
def __init__(self, case):
super(UnblindedTokenStateMachine, self).__init__()
self.case = case
self.configless = ConfiglessMemoryVoucherStore(
# Time probably not actually relevant to this state machine.
datetime.now,
)
self.configless.setUp()
self.available = 0
self.using = []
self.spent = []
self.invalid = []
def teardown(self):
self.configless.cleanUp()
@rule(voucher=vouchers(), num_passes=pass_counts())
def redeem_voucher(self, voucher, num_passes):
"""
A voucher can be redeemed, adding more unblinded tokens to the store.
"""
try:
self.configless.store.get(voucher)
except KeyError:
pass
else:
# Cannot redeem a voucher more than once. We redeemed this one
# already.
assume(False)
self.case.assertThat(
self.configless.redeem(voucher, num_passes),
succeeded(Always()),
)
self.available += num_passes
@rule(num_passes=pass_counts())
def get_passes(self, num_passes):
"""
Some passes can be requested from the store. The resulting passes are not
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
"""
assume(num_passes <= self.available)
tokens = self.configless.store.get_unblinded_tokens(num_passes)
note("get_passes: {}".format(tokens))
# No tokens we are currently using may be returned again. Nor may
# tokens which have reached a terminal state of spent or invalid.
unavailable = set(self.using) | set(self.spent) | set(self.invalid)
self.case.assertThat(
tokens,
MatchesAll(
HasLength(num_passes),
AfterPreprocessing(
lambda t: set(t) & unavailable,
Equals(set()),
),
),
)
self.using.extend(tokens)
self.available -= num_passes
@rule(excess_passes=pass_counts())
def not_enough_passes(self, excess_passes):
"""
If an attempt is made to get more passes than are available,
``get_unblinded_tokens`` raises ``NotEnoughTokens``.
"""
self.case.assertThat(
lambda: self.configless.store.get_unblinded_tokens(
self.available + excess_passes,
),
raises(NotEnoughTokens),
)
@precondition(lambda self: len(self.using) > 0)
@rule(random=randoms(), data=data())
def spend_passes(self, random, data):
"""
Some in-use passes can be discarded.
"""
self.using, to_spend = random_slice(self.using, random, data)
note("spend_passes: {}".format(to_spend))
self.configless.store.discard_unblinded_tokens(to_spend)
@precondition(lambda self: len(self.using) > 0)
@rule(random=randoms(), data=data())
def reset_passes(self, random, data):
"""
Some in-use passes can be returned to not-in-use state.
"""
self.using, to_reset = random_slice(self.using, random, data)
note("reset_passes: {}".format(to_reset))
self.configless.store.reset_unblinded_tokens(to_reset)
self.available += len(to_reset)
@precondition(lambda self: len(self.using) > 0)
@rule(random=randoms(), data=data())
def invalidate_passes(self, random, data):
"""
Some in-use passes are unusable and should be set aside.
"""
self.using, to_invalidate = random_slice(self.using, random, data)
note("invalidate_passes: {}".format(to_invalidate))
self.configless.store.invalidate_unblinded_tokens(
u"reason",
to_invalidate,
)
self.invalid.extend(to_invalidate)
@rule()
def discard_ephemeral_state(self):
"""
Reset all state that cannot outlive a single process, simulating a
restart.
XXX We have to reach into the guts of ``VoucherStore`` to do this
because we're using an in-memory database. We can't just open a new
``VoucherStore``. :/ Perhaps we should use an on-disk database... Or
maybe this is a good argument for using an explicitly attached
temporary database instead of the built-in ``temp`` database.
"""
with self.configless.store._connection:
self.configless.store._connection.execute(
"""
""",
)
self.available += len(self.using)
del self.using[:]
@invariant()
def report_state(self):
note(
"available={} using={} invalid={} spent={}".format(
self.available,
len(self.using),
len(self.invalid),
len(self.spent),
)
)
def random_slice(taken_from, random, data):
"""
Divide ``taken_from`` into two pieces with elements randomly assigned to
one piece or the other.
:param list taken_from: A list of elements to divide. This will be
mutated.
:param random: A ``random`` module-alike.
:param data: A Hypothesis data object for drawing values.
:return: A two-tuple of the two resulting lists.
"""
count = data.draw(integers(min_value=1, max_value=len(taken_from)))
random.shuffle(taken_from)
remaining = taken_from[:-count]
sliced = taken_from[-count:]
return remaining, sliced
class UnblindedTokenStateTests(TestCase):
"""
Glue ``UnblindedTokenStateTests`` into our test runner.
"""
def test_states(self):
run_state_machine_as_test(lambda: UnblindedTokenStateMachine(self))
class LeaseMaintenanceTests(TestCase):
"""
Tests for the lease-maintenance related parts of ``VoucherStore``.
"""
@given(
tahoe_configs(),
posix_safe_datetimes(),
lists(
tuples(
# How much time passes before this activity starts
timedeltas(min_value=timedelta(1), max_value=timedelta(days=1)),
# Some activity. This list of two tuples gives us a trivial
# way to compute the total passes required (just sum the pass
# counts in it). This is nice because it avoids having the
# test re-implement size quantization which would just be
# repeated code duplicating the implementation. The second
# value lets us fuzz the actual size values a little bit in a
# way which shouldn't affect the passes required.
lists(
tuples(
# The activity itself, in pass count
integers(min_value=1, max_value=2 ** 16 - 1),
# Amount by which to trim back the share sizes. This
# might exceed the value of a single pass but we don't
# know that value yet. We'll map it into a coherent
# range with mod inside the test.
integers(min_value=0),
),
),
# How much time passes before this activity finishes
timedeltas(min_value=timedelta(1), max_value=timedelta(days=1)),
),
),
)
def test_lease_maintenance_activity(self, get_config, now, activity):
"""
``VoucherStore.get_latest_lease_maintenance_activity`` returns a
``LeaseMaintenanceTests`` with fields reflecting the most recently
finished lease maintenance activity.
"""
store = self.useFixture(
TemporaryVoucherStore(get_config, lambda: now),
).store
expected = None
for (start_delay, sizes, finish_delay) in activity:
now += start_delay
started = now
x = store.start_lease_maintenance()
passes_required = 0
for (num_passes, trim_size) in sizes:
passes_required += num_passes
trim_size %= store.pass_value
x.observe(
[
num_passes * store.pass_value - trim_size,
]
)
now += finish_delay
x.finish()
finished = now
# Let the last iteration of the loop define the expected value.
expected = LeaseMaintenanceActivity(
started,
passes_required,
finished,
)
self.assertThat(
store.get_latest_lease_maintenance_activity(),
Equals(expected),
)
class VoucherTests(TestCase):
Tests for ``Voucher``.
@given(voucher_objects())
def test_json_roundtrip(self, reference):
``Voucher.to_json . Voucher.from_json → id``
Voucher.from_json(reference.to_json()),
Equals(reference),
Jean-Paul Calderone
committed
def paired_tokens(data, sizes=integers(min_value=1, max_value=1000)):
"""
Draw two lists of the same length, one of random tokens and one of
unblinded tokens.
:rtype: ([RandomTokens], [UnblindedTokens])
"""
Jean-Paul Calderone
committed
num_tokens = data.draw(sizes)
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(),
dummy_ristretto_keys(),
lists(unblinded_tokens(), unique=True),
booleans(),
)
def test_unblinded_tokens_without_voucher(
self, get_config, now, voucher_value, public_key, unblinded_tokens, completed
):
"""
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
self.assertThat(
lambda: store.insert_unblinded_tokens_for_voucher(
voucher_value,
public_key,
unblinded_tokens,
completed,
spendable=True,
),
raises(ValueError),
)
@given(
tahoe_configs(),
datetimes(),
vouchers(),
dummy_ristretto_keys(),
booleans(),
def test_unblinded_tokens_round_trip(
self, get_config, now, voucher_value, public_key, completed, data
):
Unblinded tokens that are added to the store can later be retrieved and counted.
random_tokens, unblinded_tokens = paired_tokens(data)
store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
store.add(voucher_value, len(random_tokens), 0, lambda: random_tokens)
store.insert_unblinded_tokens_for_voucher(
voucher_value, public_key, unblinded_tokens, completed, spendable=True
)
# All the tokens just inserted should be counted.
self.expectThat(
store.count_unblinded_tokens(),
Equals(len(unblinded_tokens)),
)
retrieved_tokens = store.get_unblinded_tokens(len(random_tokens))
# All the tokens just extracted should not be counted.
self.expectThat(
store.count_unblinded_tokens(),
Equals(0),
)
self.expectThat(
set(unblinded_tokens),
Equals(set(retrieved_tokens)),
)
@given(
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, public_key, num_tokens, data
):
The voucher for unblinded tokens that are added to the store is marked as
redeemed.
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, len(random), 0, lambda: random)
store.insert_unblinded_tokens_for_voucher(
voucher_value, public_key, unblinded, completed=True, spendable=True
)
loaded_voucher = store.get(voucher_value)
self.assertThat(
loaded_voucher,
MatchesStructure(
expected_tokens=Equals(len(random)),
state=Equals(
Redeemed(
finished=now,
token_count=num_tokens,
)
),
@given(
tahoe_configs(),
datetimes(),
vouchers(),
lists(random_tokens(), min_size=1, unique=True),
def test_mark_vouchers_double_spent(
self, get_config, now, voucher_value, random_tokens
):
"""
A voucher which is reported as double-spent is marked in the database as
such.
"""
store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
store.add(voucher_value, len(random_tokens), 0, lambda: random_tokens)
store.mark_voucher_double_spent(voucher_value)
voucher = store.get(voucher_value)
self.assertThat(
voucher,
MatchesStructure(
state=Equals(
DoubleSpend(
finished=now,
)
),
),
)
@given(
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, public_key, num_tokens, data
):
"""
A voucher which has already been spent cannot be marked as double-spent.
"""
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, len(random), 0, lambda: random)
store.insert_unblinded_tokens_for_voucher(
voucher_value, public_key, unblinded, completed=True, spendable=True
)
self.assertThat(
lambda: store.mark_voucher_double_spent(voucher_value),
raises(ValueError),
)
@given(
tahoe_configs(),
datetimes(),
vouchers(),
)
def test_mark_invalid_vouchers_double_spent(self, get_config, now, voucher_value):
"""
A voucher which is not known cannot be marked as double-spent.
"""
store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
self.assertThat(
lambda: store.mark_voucher_double_spent(voucher_value),
raises(ValueError),
)
@given(
tahoe_configs(),
datetimes(),
vouchers(),
dummy_ristretto_keys(),
booleans(),
integers(min_value=1),
data(),
)
def test_not_enough_unblinded_tokens(
self, get_config, now, voucher_value, public_key, completed, extra, data
):
"""
``get_unblinded_tokens`` raises ``NotEnoughTokens`` if ``count`` is
greater than the number of unblinded tokens in the store.
"""
random, unblinded = paired_tokens(data)
num_tokens = len(random)
store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
store.add(voucher_value, len(random), 0, lambda: random)
store.insert_unblinded_tokens_for_voucher(
voucher_value,
public_key,
unblinded,
completed,
spendable=True,
lambda: store.get_unblinded_tokens(num_tokens + extra),
raises(NotEnoughTokens),
)
def store_for_test(testcase, get_config, get_now):
"""
Create a ``VoucherStore`` in a temporary directory associated with the
given test case.
:param TestCase testcase: The test case for which to build the store.
:param get_config: A function like the one built by ``tahoe_configs``.
:param get_now: A no-argument callable that returns a datetime giving a
time to consider as "now".
:return VoucherStore: A newly created temporary store.
"""
tempdir = testcase.useFixture(TempDir())
config = get_config(tempdir.join(u"node"), u"tub.port")
store = VoucherStore.from_node_config(
config,
get_now,
memory_connect,
)
return store
class PassTests(TestCase):
"""
Tests for ``Pass``.
"""
@given(zkaps())
def test_roundtrip(self, pass_):
"""
``Pass`` round-trips through ``Pass.from_bytes`` and ``Pass.pass_bytes``.
"""
self.assertThat(
Pass.from_bytes(pass_.pass_bytes),
Equals(pass_),
)