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 __future__ import (
absolute_import,
)
from os import (
mkdir,
)
from errno import (
EACCES,
)
from datetime import (
timedelta,
)
from testtools import (
TestCase,
)
from testtools.matchers import (
AfterPreprocessing,
MatchesStructure,
MatchesAll,
Equals,
Raises,
IsInstance,
raises,
)
from fixtures import (
TempDir,
)
from hypothesis import (
given,
)
from hypothesis.strategies import (
tuples,
timedeltas,
from twisted.python.filepath import (
FilePath,
)
from ..storage_common import (
BYTES_PER_PASS,
)
from ..model import (
VoucherStore,
Voucher,
Pending,
DoubleSpend,
Redeemed,
LeaseMaintenanceActivity,
open_and_initialize,
memory_connect,
)
from .strategies import (
tahoe_configs,
voucher_objects,
unblinded_tokens,
posix_safe_datetimes,
from .fixtures import (
TemporaryVoucherStore,
)
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):
``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(), 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, tokens)
self.assertThat(
MatchesStructure(
state=Equals(Pending()),
@given(tahoe_configs(), vouchers(), datetimes(), lists(random_tokens(), 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
store.add(voucher, tokens)
store.add(voucher, [])
self.assertThat(
MatchesStructure(
state=Equals(Pending()),
),
)
@given(tahoe_configs(), datetimes(), lists(vouchers(), unique=True))
def test_list(self, get_config, now, vouchers):
``VoucherStore.list`` returns a ``list`` containing a ``Voucher`` object
for each voucher previously added.
store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
store.add(voucher, [])
self.assertThat(
store.list(),
Voucher(number, created=now)
for number
in vouchers
)),
@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(b"node")
# Create the node directory without permission to create the
# underlying directory.
mkdir(nodedir, 0o500)
config = get_config(nodedir, b"tub.port")
self.assertThat(
lambda: VoucherStore.from_node_config(
Raises(
AfterPreprocessing(
lambda (type, exc, tb): exc,
MatchesAll(
MatchesStructure(
reason=MatchesAll(
IsInstance(OSError),
MatchesStructure(
errno=Equals(EACCES),
),
),
),
),
),
),
)
@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(b"node")
config = get_config(nodedir, b"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),
)
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
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(0), 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
integers(min_value=0, max_value=BYTES_PER_PASS - 1),
),
),
# How much time passes before this activity finishes
timedeltas(min_value=timedelta(0), 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
x.observe([
num_passes * BYTES_PER_PASS - 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),
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):
Unblinded tokens that are added to the store can later be retrieved.
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)))
# After extraction, the unblinded tokens are no longer available.
more_unblinded_tokens = store.extract_unblinded_tokens(1)
self.expectThat([], Equals(more_unblinded_tokens))
@given(
tahoe_configs(),
datetimes(),
vouchers(),
integers(min_value=1, max_value=100),
data(),
)
def test_mark_vouchers_redeemed(self, get_config, now, voucher_value, 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, random)
store.insert_unblinded_tokens_for_voucher(voucher_value, unblinded)
loaded_voucher = store.get(voucher_value)
self.assertThat(
loaded_voucher,
MatchesStructure(
state=Equals(Redeemed(
finished=now,
token_count=num_tokens,
)),
@given(
tahoe_configs(),
datetimes(),
vouchers(),
lists(random_tokens(), 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, random_tokens)
store.mark_voucher_double_spent(voucher_value)
voucher = store.get(voucher_value)
self.assertThat(
voucher,
MatchesStructure(
state=Equals(DoubleSpend(
finished=now,
)),
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
445
446
),
)
@given(
tahoe_configs(),
datetimes(),
vouchers(),
integers(min_value=1, max_value=100),
data(),
)
def test_mark_spent_vouchers_double_spent(self, get_config, now, voucher_value, 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, random)
store.insert_unblinded_tokens_for_voucher(voucher_value, unblinded)
try:
result = store.mark_voucher_double_spent(voucher_value)
except ValueError:
pass
except Exception as e:
self.fail("mark_voucher_double_spent raised the wrong exception: {}".format(e))
else:
self.fail("mark_voucher_double_spent didn't raise, returned: {}".format(result))
@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
try:
result = store.mark_voucher_double_spent(voucher_value)
except ValueError:
pass
except Exception as e:
self.fail("mark_voucher_double_spent raised the wrong exception: {}".format(e))
else:
self.fail("mark_voucher_double_spent didn't raise, returned: {}".format(result))
# TODO: Other error states and transient states
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(b"node"), b"tub.port")
store = VoucherStore.from_node_config(
config,
get_now,
memory_connect,
)
return store