diff --git a/src/_secureaccesstokenauthorizer/model.py b/src/_secureaccesstokenauthorizer/model.py index bcdc4d636432856fc03015ff2d03a1152dfbfb50..f6da6946b47ea83945d174d18c52b9a9b6bc2d90 100644 --- a/src/_secureaccesstokenauthorizer/model.py +++ b/src/_secureaccesstokenauthorizer/model.py @@ -26,7 +26,7 @@ from json import ( ) from sqlite3 import ( - connect, + connect as _connect, ) import attr @@ -52,7 +52,26 @@ class SchemaError(TypeError): CONFIG_DB_NAME = u"privatestorageio-satauthz-v1.sqlite3" -def open_and_initialize(path): +def open_and_initialize(path, required_schema_version, connect=None): + """ + Open a SQLite3 database for use as a payment reference store. + + Create the database and populate it with a schema, if it does not already + exist. + + :param FilePath path: The location of the SQLite3 database file. + + :param int required_schema_version: The schema version which must be + present in the database in order for a SQLite3 connection to be + returned. + + :raise SchemaError: If the schema in the database does not match the + required schema version. + + :return: A SQLite3 connection object for the database at the given path. + """ + if connect is None: + connect = _connect try: path.parent().makedirs(ignoreExistingDirectory=True) except OSError as e: @@ -65,6 +84,8 @@ def open_and_initialize(path): with conn: cursor = conn.cursor() cursor.execute( + # This code knows how to create schema version 1. This is + # regardless of what the caller *wants* to find in the database. """ CREATE TABLE IF NOT EXISTS [version] AS SELECT 1 AS [version] """ @@ -74,22 +95,21 @@ def open_and_initialize(path): SELECT [version] FROM [version] """ ) - expected = [(1,)] - version = cursor.fetchall() - if version != expected: + [(actual_version,)] = cursor.fetchall() + if actual_version != required_schema_version: raise SchemaError( - "Unexpected database schema version. Expected {}. Got {}.".format( - expected, - version, + "Unexpected database schema version. Required {}. Got {}.".format( + required_schema_version, + actual_version, ), ) cursor.execute( """ CREATE TABLE IF NOT EXISTS [payment-references] ( - number text, + [number] text, - PRIMARY KEY(number) + PRIMARY KEY([number]) ) """, ) @@ -104,6 +124,13 @@ def with_cursor(f): return with_cursor +def memory_connect(path, *a, **kw): + """ + Always connect to an in-memory SQLite3 database. + """ + return _connect(":memory:", *a, **kw) + + @attr.s(frozen=True) class PaymentReferenceStore(object): """ @@ -116,10 +143,12 @@ class PaymentReferenceStore(object): _connection = attr.ib() @classmethod - def from_node_config(cls, node_config): + def from_node_config(cls, node_config, connect=None): db_path = FilePath(node_config.get_private_path(CONFIG_DB_NAME)) conn = open_and_initialize( db_path, + required_schema_version=1, + connect=connect, ) return cls( db_path, diff --git a/src/_secureaccesstokenauthorizer/resource.py b/src/_secureaccesstokenauthorizer/resource.py index cd9b06b9eef7e8fb771d88a9c794f6f15aa39a8a..9c4d60ec8ac7ab1b24d3189e19983884f1cd2779 100644 --- a/src/_secureaccesstokenauthorizer/resource.py +++ b/src/_secureaccesstokenauthorizer/resource.py @@ -44,7 +44,7 @@ from .controller import ( PaymentController, ) -def from_configuration(node_config): +def from_configuration(node_config, store=None): """ Instantiate the plugin root resource using data from its configuration section in the Tahoe-LAFS configuration file:: @@ -57,10 +57,14 @@ def from_configuration(node_config): This is also used to read and write files in the private storage area of the node's persistent state location. + :param PaymentReferenceStore store: The store to use. If ``None`` a + sensible one is constructed. + :return IResource: The root of the resource hierarchy presented by the client side of the plugin. """ - store = PaymentReferenceStore.from_node_config(node_config) + if store is None: + store = PaymentReferenceStore.from_node_config(node_config) controller = PaymentController(store) root = Resource() root.putChild( @@ -97,6 +101,8 @@ class _PaymentReferenceNumberCollection(Resource): prn = payload[u"payment-reference-number"] if not isinstance(prn, unicode): return bad_request().render(request) + if not prn.strip(): + return bad_request().render(request) try: urlsafe_b64decode(prn.encode("ascii")) except Exception: diff --git a/src/_secureaccesstokenauthorizer/tests/test_client_resource.py b/src/_secureaccesstokenauthorizer/tests/test_client_resource.py index 32c8c7731c91c576aac8a54874f7cd07e41bbcc6..d8a8d6852832bdd9ef4e167932e0b59cda65ae2f 100644 --- a/src/_secureaccesstokenauthorizer/tests/test_client_resource.py +++ b/src/_secureaccesstokenauthorizer/tests/test_client_resource.py @@ -55,6 +55,7 @@ from fixtures import ( from hypothesis import ( given, + note, ) from hypothesis.strategies import ( one_of, @@ -87,6 +88,10 @@ from treq.testing import ( RequestTraversalAgent, ) +from ..model import ( + PaymentReferenceStore, + memory_connect, +) from ..resource import ( from_configuration, ) @@ -139,6 +144,8 @@ def is_urlsafe_base64(text): return False return True + + def invalid_bodies(): """ Build byte strings that ``PUT /payment-reference-number`` considers @@ -163,6 +170,17 @@ def invalid_bodies(): binary().filter(is_not_json), ) + +def root_from_config(config): + return from_configuration( + config, + PaymentReferenceStore.from_node_config( + config, + memory_connect, + ), + ) + + class PaymentReferenceNumberTests(TestCase): """ Tests relating to ``/payment-reference-number`` as implemented by the @@ -171,7 +189,6 @@ class PaymentReferenceNumberTests(TestCase): """ def setUp(self): super(PaymentReferenceNumberTests, self).setUp() - self.tempdir = self.useFixture(TempDir()) self.useFixture(CaptureTwistedLogs()) @@ -181,10 +198,9 @@ class PaymentReferenceNumberTests(TestCase): A resource is reachable at the ``payment-reference-number`` child of a the resource returned by ``from_configuration``. """ - root = from_configuration( - get_config(self.tempdir.join(b"tahoe.ini"), b"tub.port"), - ) - + tempdir = self.useFixture(TempDir()) + config = get_config(tempdir.join(b"tahoe.ini"), b"tub.port") + root = root_from_config(config) self.assertThat( getChildForRequest(root, request), Provides([IResource]), @@ -198,9 +214,9 @@ class PaymentReferenceNumberTests(TestCase): is passed in to the PRN redemption model object for handling and an ``OK`` response is returned. """ - root = from_configuration( - get_config(self.tempdir.join(b"tahoe.ini"), b"tub.port"), - ) + tempdir = self.useFixture(TempDir()) + config = get_config(tempdir.join(b"tahoe.ini"), b"tub.port") + root = root_from_config(config) agent = RequestTraversalAgent(root) producer = FileBodyProducer( BytesIO(dumps({u"payment-reference-number": prn})), @@ -229,9 +245,9 @@ class PaymentReferenceNumberTests(TestCase): consist of an object with a single *payment-reference-number* property then the response is *BAD REQUEST*. """ - root = from_configuration( - get_config(self.tempdir.join(b"tahoe.ini"), b"tub.port"), - ) + tempdir = self.useFixture(TempDir()) + config = get_config(tempdir.join(b"tahoe.ini"), b"tub.port") + root = root_from_config(config) agent = RequestTraversalAgent(root) producer = FileBodyProducer( BytesIO(body), @@ -259,11 +275,10 @@ class PaymentReferenceNumberTests(TestCase): When a syntactically invalid PRN is requested with a ``GET`` to a child of ``PaymentReferenceNumberCollection`` the response is **BAD REQUEST**. """ + tempdir = self.useFixture(TempDir()) not_prn = prn[1:] - root = from_configuration( - get_config(self.tempdir.join(b"tahoe.ini"), b"tub.port"), - ) - + config = get_config(tempdir.join(b"tahoe.ini"), b"tub.port") + root = root_from_config(config) agent = RequestTraversalAgent(root) requesting = agent.request( b"GET", @@ -284,10 +299,9 @@ class PaymentReferenceNumberTests(TestCase): ``PaymentReferenceNumberCollection`` the response is **NOT FOUND** if the PRN hasn't previously been submitted with a ``PUT``. """ - root = from_configuration( - get_config(self.tempdir.join(b"tahoe.ini"), b"tub.port"), - ) - + tempdir = self.useFixture(TempDir()) + config = get_config(tempdir.join(b"tahoe.ini"), b"tub.port") + root = root_from_config(config) agent = RequestTraversalAgent(root) requesting = agent.request( b"GET", @@ -308,10 +322,9 @@ class PaymentReferenceNumberTests(TestCase): same PRN then the response code is **OK** and details about the PRN are included in a json-encoded response body. """ - root = from_configuration( - get_config(self.tempdir.join(b"tahoe.ini"), b"tub.port"), - ) - + tempdir = self.useFixture(TempDir()) + config = get_config(tempdir.join(b"tahoe.ini"), b"tub.port") + root = root_from_config(config) agent = RequestTraversalAgent(root) producer = FileBodyProducer( @@ -364,12 +377,12 @@ class PaymentReferenceNumberTests(TestCase): # directory for every Hypothesis iteration because this test leaves # state behind that invalidates future iterations. tempdir = self.useFixture(TempDir()) - root = from_configuration( - get_config(tempdir.join(b"tahoe.ini"), b"tub.port"), - ) - + config = get_config(tempdir.join(b"tahoe.ini"), b"tub.port") + root = root_from_config(config) agent = RequestTraversalAgent(root) + note("{} PRNs".format(len(prns))) + for prn in prns: producer = FileBodyProducer( BytesIO(dumps({u"payment-reference-number": prn})), diff --git a/src/_secureaccesstokenauthorizer/tests/test_model.py b/src/_secureaccesstokenauthorizer/tests/test_model.py index 0d3772afe2e2873baa520bf0998fb79df28b183c..2e7ba782120c41acc861705a74ffc238058435bd 100644 --- a/src/_secureaccesstokenauthorizer/tests/test_model.py +++ b/src/_secureaccesstokenauthorizer/tests/test_model.py @@ -42,16 +42,21 @@ from fixtures import ( from hypothesis import ( given, - assume, ) from hypothesis.strategies import ( lists, ) +from twisted.python.filepath import ( + FilePath, +) + from ..model import ( + SchemaError, StoreDirectoryError, - StoreAddError, PaymentReferenceStore, + open_and_initialize, + memory_connect, ) from .strategies import ( @@ -64,6 +69,22 @@ class PaymentReferenceStoreTests(TestCase): """ Tests for ``PaymentReferenceStore``. """ + 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(), payment_reference_numbers()) def test_get_missing(self, get_config, prn): """ @@ -72,7 +93,10 @@ class PaymentReferenceStoreTests(TestCase): """ tempdir = self.useFixture(TempDir()) config = get_config(tempdir.join(b"node"), b"tub.port") - store = PaymentReferenceStore.from_node_config(config) + store = PaymentReferenceStore.from_node_config( + config, + memory_connect, + ) self.assertThat( lambda: store.get(prn), raises(KeyError), @@ -87,7 +111,10 @@ class PaymentReferenceStoreTests(TestCase): """ tempdir = self.useFixture(TempDir()) config = get_config(tempdir.join(b"node"), b"tub.port") - store = PaymentReferenceStore.from_node_config(config) + store = PaymentReferenceStore.from_node_config( + config, + memory_connect, + ) store.add(prn) payment_reference = store.get(prn) self.assertThat( @@ -105,7 +132,10 @@ class PaymentReferenceStoreTests(TestCase): """ tempdir = self.useFixture(TempDir()) config = get_config(tempdir.join(b"node"), b"tub.port") - store = PaymentReferenceStore.from_node_config(config) + store = PaymentReferenceStore.from_node_config( + config, + memory_connect, + ) store.add(prn) store.add(prn) payment_reference = store.get(prn) @@ -127,7 +157,10 @@ class PaymentReferenceStoreTests(TestCase): tempdir = self.useFixture(TempDir()) nodedir = tempdir.join(b"node") config = get_config(nodedir, b"tub.port") - store = PaymentReferenceStore.from_node_config(config) + store = PaymentReferenceStore.from_node_config( + config, + memory_connect, + ) for prn in prns: store.add(prn) @@ -158,7 +191,10 @@ class PaymentReferenceStoreTests(TestCase): config = get_config(nodedir, b"tub.port") self.assertThat( - lambda: PaymentReferenceStore.from_node_config(config), + lambda: PaymentReferenceStore.from_node_config( + config, + memory_connect, + ), Raises( AfterPreprocessing( lambda (type, exc, tb): exc,