From 897ffe81bca5dc5c4856e4f81a67d69e3a2ea091 Mon Sep 17 00:00:00 2001 From: Scott Sadler Date: Thu, 19 Jan 2017 15:49:30 +0100 Subject: [PATCH] outputs endpoint with unspent filter parameter --- bigchaindb/core.py | 16 +++++++---- bigchaindb/web/routes.py | 4 +-- bigchaindb/web/views/outputs.py | 28 ++++++++++++++++++ bigchaindb/web/views/unspents.py | 23 --------------- tests/db/test_bigchain_api.py | 27 ++++++++++++++++-- tests/web/test_outputs.py | 49 ++++++++++++++++++++++++++++++++ tests/web/test_unspents.py | 24 ---------------- 7 files changed, 114 insertions(+), 57 deletions(-) create mode 100644 bigchaindb/web/views/outputs.py delete mode 100644 bigchaindb/web/views/unspents.py create mode 100644 tests/web/test_outputs.py delete mode 100644 tests/web/test_unspents.py diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 459c4908..871c3707 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -424,11 +424,17 @@ class Bigchain(object): :obj:`list` of TransactionLink: list of ``txid`` s and ``output`` s pointing to another transaction's condition """ - owned = [] - for tx_link in self.get_outputs(owner): - if not self.get_spent(tx_link.txid, tx_link.output): - owned.append(tx_link) - return owned + return self.get_outputs_filtered(owner, include_spent=False) + + def get_outputs_filtered(self, owner, include_spent=True): + """ + Get a list of output links filtered on some criteria + """ + outputs = self.get_outputs(owner) + if not include_spent: + outputs = [o for o in outputs + if not self.get_spent(o.txid, o.output)] + return outputs def get_transactions_filtered(self, asset_id, operation=None): """ diff --git a/bigchaindb/web/routes.py b/bigchaindb/web/routes.py index 18133b3e..b20f8d40 100644 --- a/bigchaindb/web/routes.py +++ b/bigchaindb/web/routes.py @@ -5,7 +5,7 @@ from bigchaindb.web.views import ( info, statuses, transactions as tx, - unspents, + outputs, votes, ) @@ -30,7 +30,7 @@ ROUTES_API_V1 = [ r('statuses/', statuses.StatusApi), r('transactions/', tx.TransactionApi), r('transactions', tx.TransactionListApi), - r('unspents/', unspents.UnspentListApi), + r('outputs/', outputs.OutputListApi), r('votes/', votes.VotesApi), ] diff --git a/bigchaindb/web/views/outputs.py b/bigchaindb/web/views/outputs.py new file mode 100644 index 00000000..735a428f --- /dev/null +++ b/bigchaindb/web/views/outputs.py @@ -0,0 +1,28 @@ +from flask import current_app +from flask_restful import reqparse, Resource + +from bigchaindb.web.views import parameters + + +class OutputListApi(Resource): + def get(self): + """API endpoint to retrieve a list of links to transaction + outputs. + + Returns: + A :obj:`list` of :cls:`str` of links to outputs. + """ + parser = reqparse.RequestParser() + parser.add_argument('public_key', type=parameters.valid_ed25519, + required=True) + parser.add_argument('unspent', type=parameters.valid_bool) + args = parser.parse_args() + + pool = current_app.config['bigchain_pool'] + include_spent = not args['unspent'] + + with pool() as bigchain: + outputs = bigchain.get_outputs_filtered(args['public_key'], + include_spent) + # NOTE: We pass '..' as a path to create a valid relative URI + return [u.to_uri('..') for u in outputs] diff --git a/bigchaindb/web/views/unspents.py b/bigchaindb/web/views/unspents.py deleted file mode 100644 index 8cca995f..00000000 --- a/bigchaindb/web/views/unspents.py +++ /dev/null @@ -1,23 +0,0 @@ -from flask import current_app -from flask_restful import reqparse, Resource - - -class UnspentListApi(Resource): - def get(self): - """API endpoint to retrieve a list of links to transactions's - conditions that have not been used in any previous transaction. - - Returns: - A :obj:`list` of :cls:`str` of links to unfulfilled conditions. - """ - parser = reqparse.RequestParser() - parser.add_argument('public_key', type=str, location='args', - required=True) - args = parser.parse_args() - - pool = current_app.config['bigchain_pool'] - - with pool() as bigchain: - unspents = bigchain.get_owned_ids(args['public_key']) - # NOTE: We pass '..' as a path to create a valid relative URI - return [u.to_uri('..') for u in unspents] diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index cfc2e93f..78c14a28 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -1159,13 +1159,34 @@ class TestMultipleInputs(object): assert b.get_spent(unspent.id, 0) is None -def test_get_owned_ids_calls(): +def test_get_owned_ids_calls_get_outputs_filtered(): + from bigchaindb.core import Bigchain + with patch('bigchaindb.core.Bigchain.get_outputs_filtered') as gof: + b = Bigchain() + res = b.get_owned_ids("abc") + gof.assert_called_once_with("abc", include_spent=False) + assert res == gof() + + +def test_get_outputs_filtered_only_unspent(): from bigchaindb.common.transaction import TransactionLink as TL from bigchaindb.core import Bigchain with patch('bigchaindb.core.Bigchain.get_outputs') as get_outputs: get_outputs.return_value = [TL('a', 1), TL('b', 2)] with patch('bigchaindb.core.Bigchain.get_spent') as get_spent: get_spent.side_effect = [True, False] - out = Bigchain().get_owned_ids('abc') - assert get_outputs.called_once_with('abc') + out = Bigchain().get_outputs_filtered('abc', include_spent=False) + get_outputs.assert_called_once_with('abc') assert out == [TL('b', 2)] + + +def test_get_outputs_filtered(): + from bigchaindb.common.transaction import TransactionLink as TL + from bigchaindb.core import Bigchain + with patch('bigchaindb.core.Bigchain.get_outputs') as get_outputs: + get_outputs.return_value = [TL('a', 1), TL('b', 2)] + with patch('bigchaindb.core.Bigchain.get_spent') as get_spent: + out = Bigchain().get_outputs_filtered('abc') + get_outputs.assert_called_once_with('abc') + get_spent.assert_not_called() + assert out == get_outputs.return_value diff --git a/tests/web/test_outputs.py b/tests/web/test_outputs.py new file mode 100644 index 00000000..8fb418ea --- /dev/null +++ b/tests/web/test_outputs.py @@ -0,0 +1,49 @@ +import pytest +from unittest.mock import MagicMock, patch + +pytestmark = [pytest.mark.bdb, pytest.mark.usefixtures('inputs')] + +UNSPENTS_ENDPOINT = '/api/v1/outputs/' + + +def test_get_outputs_endpoint(client, user_pk): + m = MagicMock() + m.to_uri.side_effect = lambda s: s + with patch('bigchaindb.core.Bigchain.get_outputs_filtered') as gof: + gof.return_value = [m, m] + res = client.get(UNSPENTS_ENDPOINT + '?public_key={}'.format(user_pk)) + assert res.json == ["..", ".."] + assert res.status_code == 200 + gof.assert_called_once_with(user_pk, True) + + +def test_get_outputs_endpoint_unspent(client, user_pk): + m = MagicMock() + m.to_uri.side_effect = lambda s: s + with patch('bigchaindb.core.Bigchain.get_outputs_filtered') as gof: + gof.return_value = [m] + params = '?unspent=true&public_key={}'.format(user_pk) + res = client.get(UNSPENTS_ENDPOINT + params) + assert res.json == [".."] + assert res.status_code == 200 + gof.assert_called_once_with(user_pk, False) + + +def test_get_outputs_endpoint_without_public_key(client): + res = client.get(UNSPENTS_ENDPOINT) + assert res.status_code == 400 + + +def test_get_outputs_endpoint_with_invalid_public_key(client): + expected = {'message': {'public_key': 'Invalid base58 ed25519 key'}} + res = client.get(UNSPENTS_ENDPOINT + '?public_key=abc') + assert expected == res.json + assert res.status_code == 400 + + +def test_get_outputs_endpoint_with_invalid_unspent(client, user_pk): + expected = {'message': {'unspent': 'Boolean value must be "true" or "false" (lowercase)'}} + params = '?unspent=tru&public_key={}'.format(user_pk) + res = client.get(UNSPENTS_ENDPOINT + params) + assert expected == res.json + assert res.status_code == 400 diff --git a/tests/web/test_unspents.py b/tests/web/test_unspents.py deleted file mode 100644 index 9539c664..00000000 --- a/tests/web/test_unspents.py +++ /dev/null @@ -1,24 +0,0 @@ -import pytest - -pytestmark = [pytest.mark.bdb, pytest.mark.usefixtures('inputs')] - -UNSPENTS_ENDPOINT = '/api/v1/unspents/' - - -def test_get_unspents_endpoint(b, client, user_pk): - expected = [u.to_uri('..') for u in b.get_owned_ids(user_pk)] - res = client.get(UNSPENTS_ENDPOINT + '?public_key={}'.format(user_pk)) - assert expected == res.json - assert res.status_code == 200 - - -def test_get_unspents_endpoint_without_public_key(client): - res = client.get(UNSPENTS_ENDPOINT) - assert res.status_code == 400 - - -def test_get_unspents_endpoint_with_unused_public_key(client): - expected = [] - res = client.get(UNSPENTS_ENDPOINT + '?public_key=abc') - assert expected == res.json - assert res.status_code == 200