diff --git a/docs/source/interface.rst b/docs/source/interface.rst index 0dcccabc1590e6974aeb6a7147cc5e7389123907..1b3c8543d576e1fc6d10dfbd47307ecfcdae958e 100644 --- a/docs/source/interface.rst +++ b/docs/source/interface.rst @@ -42,3 +42,15 @@ The ``stage`` property indicates how far into redemption the plugin has proceede The ``of`` property indicates how many steps the process involves in total. The ``stage-name`` property gives a human-meaningful description of the current stage. The ``stage-entered-time`` property gives the timestamp for the start of the current staged. + +``GET /storage-plugins/privatestorageio-satauthz-v1/payment-reference-number`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This endpoint allows an external agent to retrieve the status of all payment reference numbers. +This endpoint accepts no request body. + +The response is **OK** with ``application/json`` content-type response body like:: + + {"payment-reference-numbers": [<payment reference status object>, ...]} + +The elements of the list are objects like the one returned by issuing a **GET** to a child of this collection resource. diff --git a/src/_secureaccesstokenauthorizer/model.py b/src/_secureaccesstokenauthorizer/model.py index 915481bb646619f754a582a371165f4e8385261d..2e852c42f1f1e365eba39e4ff945b3774e9fb0c0 100644 --- a/src/_secureaccesstokenauthorizer/model.py +++ b/src/_secureaccesstokenauthorizer/model.py @@ -19,9 +19,11 @@ the storage plugin. from os import ( makedirs, + listdir, ) from errno import ( EEXIST, + ENOENT, ) from json import ( loads, @@ -59,6 +61,11 @@ class PaymentReferenceStore(object): def _config_key(self, prn): return u"{}/{}.prn+json".format(self._CONFIG_DIR, prn) + def _prn(self, config_key): + if config_key.endswith(u".prn+json"): + return config_key[:-len(u".prn+json")] + raise ValueError("{} does not look like a config key".format(config_key)) + def _read_pr_json(self, prn): private_config_item = self._config_key(prn) try: @@ -93,6 +100,22 @@ class PaymentReferenceStore(object): except KeyError: self._write_pr_json(prn, PaymentReference(prn).to_json()) + def list(self): + # XXX Need an API to be able to avoid touching the filesystem directly + # here. + container = self.node_config.get_private_path(self._CONFIG_DIR) + try: + children = listdir(container) + except EnvironmentError as e: + if ENOENT != e.errno: + raise + children = [] + return list( + PaymentReference(self._prn(config_key)) + for config_key + in children + ) + @attr.s class PaymentReference(object): @@ -111,7 +134,11 @@ class PaymentReference(object): def to_json(self): - return dumps(self.to_json_v1()) + return dumps(self.marshal()) + + + def marshal(self): + return self.to_json_v1() def to_json_v1(self): diff --git a/src/_secureaccesstokenauthorizer/resource.py b/src/_secureaccesstokenauthorizer/resource.py index f76f0a6fabf072f45af6cba8120f16cd396edd6b..49b37f5b8b9038d352ac469749d289da75389523 100644 --- a/src/_secureaccesstokenauthorizer/resource.py +++ b/src/_secureaccesstokenauthorizer/resource.py @@ -21,7 +21,7 @@ In the future it should also allow users to read statistics about token usage. """ from json import ( - loads, + loads, dumps, ) from twisted.web.http import ( @@ -106,6 +106,17 @@ class _PaymentReferenceNumberCollection(Resource): return b"" + def render_GET(self, request): + request.responseHeaders.setRawHeaders(u"content-type", [u"application/json"]) + return dumps({ + u"payment-reference-numbers": list( + prn.marshal() + for prn + in self._store.list() + ), + }) + + def getChild(self, segment, request): prn = segment try: diff --git a/src/_secureaccesstokenauthorizer/tests/strategies.py b/src/_secureaccesstokenauthorizer/tests/strategies.py index 7da47c2b72185eacd304f0ca27bf69597f15dcc9..4af061defb0f2d34a3912078e55dfd76e580d205 100644 --- a/src/_secureaccesstokenauthorizer/tests/strategies.py +++ b/src/_secureaccesstokenauthorizer/tests/strategies.py @@ -170,6 +170,8 @@ def payment_reference_numbers(): max_size=32, ).map( urlsafe_b64encode, + ).map( + lambda prn: prn.decode("ascii"), ) diff --git a/src/_secureaccesstokenauthorizer/tests/test_client_resource.py b/src/_secureaccesstokenauthorizer/tests/test_client_resource.py index 30b58c0004da7053b6a84b4d587a66cc7c3cb759..32c8c7731c91c576aac8a54874f7cd07e41bbcc6 100644 --- a/src/_secureaccesstokenauthorizer/tests/test_client_resource.py +++ b/src/_secureaccesstokenauthorizer/tests/test_client_resource.py @@ -60,6 +60,7 @@ from hypothesis.strategies import ( one_of, just, fixed_dictionaries, + lists, integers, binary, text, @@ -352,6 +353,66 @@ class PaymentReferenceNumberTests(TestCase): ), ) + @given(tahoe_configs_with_client_config, lists(payment_reference_numbers(), unique=True)) + def test_list_prns(self, get_config, prns): + """ + A ``GET`` to the ``PaymentReferenceNumberCollection`` itself returns a + list of existing payment reference numbers. + """ + # Hypothesis causes our test case instances to be re-used many times + # between setUp and tearDown. Avoid re-using the same temporary + # 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"), + ) + + agent = RequestTraversalAgent(root) + + for prn in prns: + producer = FileBodyProducer( + BytesIO(dumps({u"payment-reference-number": prn})), + cooperator=uncooperator(), + ) + putting = agent.request( + b"PUT", + b"http://127.0.0.1/payment-reference-number", + bodyProducer=producer, + ) + self.assertThat( + putting, + succeeded( + ok_response(), + ), + ) + + getting = agent.request( + b"GET", + b"http://127.0.0.1/payment-reference-number", + ) + + self.assertThat( + getting, + succeeded( + MatchesAll( + ok_response(headers=application_json()), + AfterPreprocessing( + json_content, + succeeded( + Equals({ + u"payment-reference-numbers": list( + {u"version": 1, u"number": prn} + for prn + in prns + ), + }), + ), + ), + ), + ), + ) + def application_json(): return AfterPreprocessing( diff --git a/src/_secureaccesstokenauthorizer/tests/test_model.py b/src/_secureaccesstokenauthorizer/tests/test_model.py index 2b875b94856ba167f961e2a8e9750755dd4b1501..1159133d7a6fe8d0fd8834ad423af7e7b0e94551 100644 --- a/src/_secureaccesstokenauthorizer/tests/test_model.py +++ b/src/_secureaccesstokenauthorizer/tests/test_model.py @@ -45,6 +45,9 @@ from hypothesis import ( given, assume, ) +from hypothesis.strategies import ( + lists, +) from ..model import ( StoreDirectoryError, @@ -115,6 +118,30 @@ class PaymentReferenceStoreTests(TestCase): ) + @given(tahoe_configs(), lists(payment_reference_numbers())) + def test_list(self, get_config, prns): + """ + ``PaymentReferenceStore.list`` returns a ``list`` containing a + ``PaymentReference`` object for each payment reference number + previously added. + """ + tempdir = self.useFixture(TempDir()) + nodedir = tempdir.join(b"node") + config = get_config(nodedir, b"tub.port") + store = PaymentReferenceStore(config) + + for prn in prns: + store.add(prn) + + self.assertThat( + store.list(), + AfterPreprocessing( + lambda refs: set(ref.number for ref in refs), + Equals(set(prns)), + ), + ) + + @given(tahoe_configs(), payment_reference_numbers(), payment_reference_numbers()) def test_unwriteable_store_directory(self, get_config, prn_a, prn_b): """