diff --git a/bigchaindb/web/routes.py b/bigchaindb/web/routes.py index f6ee1886..c159f3a9 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, @@ -23,6 +24,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..bb415a3e --- /dev/null +++ b/bigchaindb/web/views/blocks.py @@ -0,0 +1,62 @@ +"""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.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 and statuses of a transaction. + + Return: + A ``dict`` in the format ``{'block_id': }``, where + ```` is one of "valid", "invalid", "undecided", "backlog". + It's possible to return multiple keys of 'block_id'. + """ + parser = reqparse.RequestParser() + parser.add_argument('tx_id', type=str) + + args = parser.parse_args(strict=True) + + if sum(arg is not None for arg in args.values()) != 1: + return make_error(400, "Provide exactly one query parameter. Choices are: block_id, tx_id") + + pool = current_app.config['bigchain_pool'] + blocks = None + + with pool() as bigchain: + if args['tx_id']: + blocks = bigchain.get_blocks_status_containing_tx(args['tx_id']) + + if not blocks: + return make_error(404) + + return blocks diff --git a/tests/web/test_blocks.py b/tests/web/test_blocks.py new file mode 100644 index 00000000..5587442c --- /dev/null +++ b/tests/web/test_blocks.py @@ -0,0 +1,104 @@ +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 res.json[block_invalid.id] == b.block_election_status(block_invalid.id, block_invalid.voters) + assert res.json[block_invalid.id] == 'undecided' + 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 res.json[block_invalid.id] == b.block_election_status(block_invalid.id, block_invalid.voters) + assert res.json[block_invalid.id] == 'invalid' + 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 res.json[block_valid.id] == b.block_election_status(block_valid.id, block_valid.voters) + assert res.json[block_valid.id] == 'undecided' + 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 res.json[block_valid.id] == b.block_election_status(block_valid.id, block_valid.voters) + assert res.json[block_valid.id] == 'valid' + assert len(res.json) == 2 + + +@pytest.mark.bdb +def test_get_blocks_by_txid_endpoint_returns_404_if_not_found(client): + res = client.get(BLOCKS_ENDPOINT + "?tx_id=123") + assert res.status_code == 404 + + +@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 + + res = client.get(BLOCKS_ENDPOINT + "?tx_id=123&block_id=123") + assert res.status_code == 400