diff --git a/bigchaindb/web/routes.py b/bigchaindb/web/routes.py index 041acc4e..18133b3e 100644 --- a/bigchaindb/web/routes.py +++ b/bigchaindb/web/routes.py @@ -1,6 +1,7 @@ """ API routes definition """ from flask_restful import Api from bigchaindb.web.views import ( + blocks, info, statuses, transactions as tx, @@ -24,6 +25,8 @@ def r(*args, **kwargs): ROUTES_API_V1 = [ r('/', info.ApiV1Index), + r('blocks/', blocks.BlockApi), + r('blocks/', blocks.BlockListApi), r('statuses/', statuses.StatusApi), r('transactions/', tx.TransactionApi), r('transactions', tx.TransactionListApi), diff --git a/bigchaindb/web/views/blocks.py b/bigchaindb/web/views/blocks.py new file mode 100644 index 00000000..7e840fe5 --- /dev/null +++ b/bigchaindb/web/views/blocks.py @@ -0,0 +1,61 @@ +"""This module provides the blueprint for the blocks API endpoints. + +For more information please refer to the documentation on ReadTheDocs: + - https://docs.bigchaindb.com/projects/server/en/latest/drivers-clients/ + http-client-server-api.html +""" +from flask import current_app +from flask_restful import Resource, reqparse + +from bigchaindb import Bigchain +from bigchaindb.web.views.base import make_error + + +class BlockApi(Resource): + def get(self, block_id): + """API endpoint to get details about a block. + + Args: + block_id (str): the id of the block. + + Return: + A JSON string containing the data about the block. + """ + + pool = current_app.config['bigchain_pool'] + + with pool() as bigchain: + block = bigchain.get_block(block_id=block_id) + + if not block: + return make_error(404) + + return block + + +class BlockListApi(Resource): + def get(self): + """API endpoint to get the related blocks for a transaction. + + Return: + A ``list`` of ``block_id``s that contain the given transaction. The + list may be filtered when provided a status query parameter: + "valid", "invalid", "undecided". + """ + parser = reqparse.RequestParser() + parser.add_argument('tx_id', type=str, required=True) + parser.add_argument('status', type=str, + choices=[Bigchain.BLOCK_VALID, Bigchain.BLOCK_INVALID, Bigchain.BLOCK_UNDECIDED]) + + args = parser.parse_args(strict=True) + tx_id = args['tx_id'] + status = args['status'] + + pool = current_app.config['bigchain_pool'] + + with pool() as bigchain: + block_statuses = bigchain.get_blocks_status_containing_tx(tx_id) + blocks = [block_id for block_id, block_status in block_statuses.items() + if not status or block_status == status] + + return blocks diff --git a/tests/web/test_blocks.py b/tests/web/test_blocks.py new file mode 100644 index 00000000..b0581061 --- /dev/null +++ b/tests/web/test_blocks.py @@ -0,0 +1,178 @@ +import pytest + +from bigchaindb.models import Transaction + +BLOCKS_ENDPOINT = '/api/v1/blocks/' + + +@pytest.mark.bdb +@pytest.mark.usefixtures('inputs') +def test_get_block_endpoint(b, client): + tx = Transaction.create([b.me], [([b.me], 1)]) + tx = tx.sign([b.me_private]) + + block = b.create_block([tx]) + b.write_block(block) + + res = client.get(BLOCKS_ENDPOINT + block.id) + assert block.to_dict() == res.json + assert res.status_code == 200 + + +@pytest.mark.bdb +@pytest.mark.usefixtures('inputs') +def test_get_block_returns_404_if_not_found(client): + res = client.get(BLOCKS_ENDPOINT + '123') + assert res.status_code == 404 + + res = client.get(BLOCKS_ENDPOINT + '123/') + assert res.status_code == 404 + + +@pytest.mark.bdb +@pytest.mark.usefixtures('inputs') +def test_get_blocks_by_txid_endpoint(b, client): + tx = Transaction.create([b.me], [([b.me], 1)]) + tx = tx.sign([b.me_private]) + + tx2 = Transaction.create([b.me], [([b.me], 10)]) + tx2 = tx2.sign([b.me_private]) + + block_invalid = b.create_block([tx]) + b.write_block(block_invalid) + + res = client.get(BLOCKS_ENDPOINT + "?tx_id=" + tx.id) + # test if block is retrieved as undecided + assert res.status_code == 200 + assert block_invalid.id in res.json + assert len(res.json) == 1 + + # vote the block invalid + vote = b.vote(block_invalid.id, b.get_last_voted_block().id, False) + b.write_vote(vote) + + res = client.get(BLOCKS_ENDPOINT + "?tx_id=" + tx.id) + # test if block is retrieved as invalid + assert res.status_code == 200 + assert block_invalid.id in res.json + assert len(res.json) == 1 + + # create a new block containing the same tx (and tx2 to avoid block id collision) + block_valid = b.create_block([tx, tx2]) + b.write_block(block_valid) + + res = client.get(BLOCKS_ENDPOINT + "?tx_id=" + tx.id) + # test if block is retrieved as undecided + assert res.status_code == 200 + assert block_valid.id in res.json + assert len(res.json) == 2 + + # vote the block valid + vote = b.vote(block_valid.id, block_invalid.id, True) + b.write_vote(vote) + + res = client.get(BLOCKS_ENDPOINT + "?tx_id=" + tx.id) + # test if block is retrieved as valid + assert res.status_code == 200 + assert block_valid.id in res.json + assert len(res.json) == 2 + + +@pytest.mark.bdb +@pytest.mark.usefixtures('inputs') +def test_get_blocks_by_txid_and_status_endpoint(b, client): + from bigchaindb import Bigchain + + tx = Transaction.create([b.me], [([b.me], 1)]) + tx = tx.sign([b.me_private]) + + tx2 = Transaction.create([b.me], [([b.me], 10)]) + tx2 = tx2.sign([b.me_private]) + + block_invalid = b.create_block([tx]) + b.write_block(block_invalid) + + # create a new block containing the same tx (and tx2 to avoid block id collision) + block_valid = b.create_block([tx, tx2]) + b.write_block(block_valid) + + res = client.get("{}?tx_id={}&status={}".format(BLOCKS_ENDPOINT, tx.id, Bigchain.BLOCK_INVALID)) + # test if no blocks are retrieved as invalid + assert res.status_code == 200 + assert len(res.json) == 0 + + res = client.get("{}?tx_id={}&status={}".format(BLOCKS_ENDPOINT, tx.id, Bigchain.BLOCK_UNDECIDED)) + # test if both blocks are retrieved as undecided + assert res.status_code == 200 + assert block_valid.id in res.json + assert block_invalid.id in res.json + assert len(res.json) == 2 + + res = client.get("{}?tx_id={}&status={}".format(BLOCKS_ENDPOINT, tx.id, Bigchain.BLOCK_VALID)) + # test if no blocks are retrieved as valid + assert res.status_code == 200 + assert len(res.json) == 0 + + # vote one of the blocks invalid + vote = b.vote(block_invalid.id, b.get_last_voted_block().id, False) + b.write_vote(vote) + + # vote the other block valid + vote = b.vote(block_valid.id, block_invalid.id, True) + b.write_vote(vote) + + res = client.get("{}?tx_id={}&status={}".format(BLOCKS_ENDPOINT, tx.id, Bigchain.BLOCK_INVALID)) + # test if the invalid block is retrieved as invalid + assert res.status_code == 200 + assert block_invalid.id in res.json + assert len(res.json) == 1 + + res = client.get("{}?tx_id={}&status={}".format(BLOCKS_ENDPOINT, tx.id, Bigchain.BLOCK_UNDECIDED)) + # test if no blocks are retrieved as undecided + assert res.status_code == 200 + assert len(res.json) == 0 + + res = client.get("{}?tx_id={}&status={}".format(BLOCKS_ENDPOINT, tx.id, Bigchain.BLOCK_VALID)) + # test if the valid block is retrieved as valid + assert res.status_code == 200 + assert block_valid.id in res.json + assert len(res.json) == 1 + + +@pytest.mark.bdb +def test_get_blocks_by_txid_endpoint_returns_empty_list_not_found(client): + res = client.get(BLOCKS_ENDPOINT + "?tx_id=") + assert res.status_code == 200 + assert len(res.json) == 0 + + res = client.get(BLOCKS_ENDPOINT + "?tx_id=123") + assert res.status_code == 200 + assert len(res.json) == 0 + + +@pytest.mark.bdb +def test_get_blocks_by_txid_endpoint_returns_400_bad_query_params(client): + res = client.get(BLOCKS_ENDPOINT) + assert res.status_code == 400 + + res = client.get(BLOCKS_ENDPOINT + "?ts_id=123") + assert res.status_code == 400 + assert res.json == { + 'message': { + 'tx_id': 'Missing required parameter in the JSON body or the post body or the query string' + } + } + + res = client.get(BLOCKS_ENDPOINT + "?tx_id=123&foo=123") + assert res.status_code == 400 + assert res.json == { + 'message': 'Unknown arguments: foo' + } + + res = client.get(BLOCKS_ENDPOINT + "?tx_id=123&status=123") + assert res.status_code == 400 + assert res.json == { + 'message': { + 'status': '123 is not a valid choice' + } + }