mirror of
https://github.com/Blockstream/satellite-api.git
synced 2025-03-13 11:35:20 +01:00
The same server now can handle multiple logical channels, on which the transmitter logic runs independently. That is, while previously a single message would be in transmitting state at a time, now multiple messages can be in transmitting state as long as they belong to distinct logical channels. The supported channels each have different permissions. The user channel is where users can post, get, and delete messages as needed. In contrast, the other channels do not grant all permissions to users. Some are read-only (users can get but not post) and there is a channel (the auth channel) on which users have no permissions (neither get nor post). For the channels on which users do not have all permissions (get, post, and delete), this patch adds admin-specific routes, which are prefixed by /admin/. The /admin/ route is protected via SSL in production and allows the admin host to send GET/POST/DELETE requests normally. Hence, for instance, the admin host can post a message on the auth channel (with POST /admin/order) and read it (with GET /admin/order) for transmission over satellite, whereas regulars cannot. With this scheme, the auth channel messages are accessible exclusively over satellite (and not over the internet). The admin routes were added to the following endpoints: - /order/<uuid> (GET and DELETE requests) - /order (POST request) - /orders/<state> (GET request) - /message/<tx_seq_num> (GET request) The messages posted by the admin are not paid, so this patch removes the requirement of invoice generation and payment. Only paid orders now generate an invoice. Thus, the POST request to the /order/ endpoint does not return an invoice for non-paid (admin-only) messages. Also, this patch updates the queue page to display the orders separately for each channel. The query string channel parameter determines which channel the page shows. Finally, this patch updates the events published into the Redis db on transmission. The event includes the corresponding logical channel so that SSE events can be subscribed independently for each channel.
415 lines
17 KiB
Python
415 lines
17 KiB
Python
import pytest
|
|
import random
|
|
from datetime import timedelta
|
|
from http import HTTPStatus
|
|
from time import sleep
|
|
from unittest.mock import patch
|
|
|
|
from constants import InvoiceStatus, OrderStatus
|
|
from database import db
|
|
from models import Order
|
|
from invoice_helpers import pay_invoice
|
|
import bidding
|
|
import constants
|
|
import server
|
|
|
|
from common import new_invoice, place_order, generate_test_order
|
|
from error import assert_error
|
|
|
|
|
|
@pytest.fixture
|
|
def client(mockredis):
|
|
app = server.create_app(from_test=True)
|
|
app.app_context().push()
|
|
with app.test_client() as client:
|
|
yield client
|
|
server.teardown_app(app)
|
|
|
|
|
|
@pytest.mark.parametrize("state", ['pending', 'queued', 'sent'])
|
|
def test_get_orders_invalid_before_parameter(client, state):
|
|
get_rv = client.get(f'/orders/{state}?before=')
|
|
assert get_rv.status_code == HTTPStatus.BAD_REQUEST
|
|
get_rv = client.get(f'/orders/{state}?before=sometext')
|
|
assert get_rv.status_code == HTTPStatus.BAD_REQUEST
|
|
get_rv = client.get(f'/orders/{state}?before=2021-13-11')
|
|
assert get_rv.status_code == HTTPStatus.BAD_REQUEST
|
|
get_rv = client.get(f'/orders/{state}?before=2021.05.10')
|
|
assert get_rv.status_code == HTTPStatus.BAD_REQUEST
|
|
get_rv = client.get(f'/orders/{state}?before=2021-05-1a0T19:51:45')
|
|
assert get_rv.status_code == HTTPStatus.BAD_REQUEST
|
|
get_rv = client.get(f'/orders/{state}?before=2021.05.10T19:51:45')
|
|
assert get_rv.status_code == HTTPStatus.BAD_REQUEST
|
|
get_rv = client.get(f'/orders/{state}?before=2021-05-10T25:51:45')
|
|
assert get_rv.status_code == HTTPStatus.BAD_REQUEST
|
|
|
|
|
|
@pytest.mark.parametrize("state", ['pending', 'queued', 'sent'])
|
|
def test_get_orders_invalid_limit(client, state):
|
|
get_rv = client.get(f'/orders/{state}?limit=')
|
|
assert get_rv.status_code == HTTPStatus.BAD_REQUEST
|
|
get_rv = client.get(f'/orders/{state}?limit=sometext')
|
|
assert get_rv.status_code == HTTPStatus.BAD_REQUEST
|
|
get_rv = client.get(f'/orders/{state}?limit=1a2')
|
|
assert get_rv.status_code == HTTPStatus.BAD_REQUEST
|
|
get_rv = client.get(f'/orders/{state}?limit=-1')
|
|
assert get_rv.status_code == HTTPStatus.BAD_REQUEST
|
|
get_rv = client.get(f'/orders/{state}?limit=1.2')
|
|
assert get_rv.status_code == HTTPStatus.BAD_REQUEST
|
|
|
|
|
|
def test_try_to_get_invalid_order_state(client):
|
|
for state in ['pendiing', 'someendpoint', 'Pending']:
|
|
get_rv = client.get(f'/orders/{state}')
|
|
assert get_rv.status_code == HTTPStatus.NOT_FOUND
|
|
|
|
|
|
@patch('constants.PAGE_SIZE', 3) # change PAGE_SIZE to run this test faster
|
|
@patch('orders.new_invoice')
|
|
@pytest.mark.parametrize("state", ['pending', 'queued', 'sent'])
|
|
def test_get_orders_before_parameter(mock_new_invoice, client, state):
|
|
# Create PAGE_SIZE orders with the target state
|
|
n_orders = constants.PAGE_SIZE
|
|
n_bytes = 500
|
|
mock_new_invoice.return_value = (True,
|
|
new_invoice(1, InvoiceStatus.pending,
|
|
bidding.get_min_bid(n_bytes)))
|
|
order_uuids = []
|
|
for i in range(n_orders):
|
|
post_rv = place_order(client, n_bytes)
|
|
assert post_rv.status_code == HTTPStatus.OK
|
|
uuid = post_rv.get_json()['uuid']
|
|
order_uuids.append(uuid)
|
|
db_order = Order.query.filter_by(uuid=uuid).first()
|
|
db_order.status = OrderStatus.transmitting.value if \
|
|
state == 'queued' else OrderStatus[state].value
|
|
db.session.commit()
|
|
sleep(1.0) # to have different created_at
|
|
|
|
# Fetch orders excluding the last
|
|
last_db_order = Order.query.filter_by(uuid=order_uuids[-1]).first()
|
|
last_created_at = last_db_order.created_at.isoformat()
|
|
|
|
get_rv = client.get(f'/orders/{state}?before={last_created_at}')
|
|
assert get_rv.status_code == HTTPStatus.OK
|
|
fetched_uuids = [order['uuid'] for order in get_rv.get_json()]
|
|
|
|
# The last order should be filtered out by the before filter
|
|
expected_uuids = order_uuids[:-1]
|
|
# Expected sorting: /orders/pending endpoint sorts by the created_at
|
|
# timestamp, /orders/queued by the bid_per_byte ratio, and /orders/sent by
|
|
# the transmission_at timestamp.
|
|
if (state == 'pending'):
|
|
expected_uuids.reverse()
|
|
|
|
assert len(fetched_uuids) == n_orders - 1
|
|
assert fetched_uuids == expected_uuids
|
|
|
|
|
|
@patch('orders.new_invoice')
|
|
@pytest.mark.parametrize("state", ['pending', 'queued', 'sent'])
|
|
def test_get_orders_limit_parameter(mock_new_invoice, client, state):
|
|
n_bytes = 500
|
|
mock_new_invoice.return_value = (True,
|
|
new_invoice(1, InvoiceStatus.pending,
|
|
bidding.get_min_bid(n_bytes)))
|
|
for i in range(constants.MAX_PAGE_SIZE + 1):
|
|
post_rv = place_order(client, n_bytes)
|
|
assert post_rv.status_code == HTTPStatus.OK
|
|
uuid = post_rv.get_json()['uuid']
|
|
db_order = Order.query.filter_by(uuid=uuid).first()
|
|
db_order.status = OrderStatus.transmitting.value if \
|
|
state == 'queued' else OrderStatus[state].value
|
|
db.session.commit()
|
|
|
|
# no limit parameter, max PAGE_SIZE should be returned
|
|
get_rv = client.get(f'/orders/{state}')
|
|
assert get_rv.status_code == HTTPStatus.OK
|
|
get_json_resp = get_rv.get_json()
|
|
assert len(get_json_resp) == constants.PAGE_SIZE
|
|
|
|
# with limit parameter present
|
|
get_rv = client.get(f'/orders/{state}?limit=10')
|
|
assert get_rv.status_code == HTTPStatus.OK
|
|
get_json_resp = get_rv.get_json()
|
|
assert len(get_json_resp) == 10
|
|
|
|
# the limit should be within [1, MAX_PAGE_SIZE]
|
|
get_rv = client.get(f'/orders/{state}?limit={constants.MAX_PAGE_SIZE}')
|
|
assert get_rv.status_code == HTTPStatus.OK
|
|
get_json_resp = get_rv.get_json()
|
|
assert len(get_json_resp) == constants.MAX_PAGE_SIZE
|
|
get_rv = client.get(f'/orders/{state}?limit={constants.MAX_PAGE_SIZE + 1}')
|
|
assert get_rv.status_code == HTTPStatus.BAD_REQUEST
|
|
get_rv = client.get(f'/orders/{state}?limit=0')
|
|
assert get_rv.status_code == HTTPStatus.BAD_REQUEST
|
|
|
|
|
|
@patch('orders.new_invoice')
|
|
@pytest.mark.parametrize("state", ['pending', 'queued', 'sent'])
|
|
@pytest.mark.parametrize("channel", constants.CHANNELS)
|
|
def test_get_orders_channel_parameter(mock_new_invoice, client, state,
|
|
channel):
|
|
n_bytes = 500
|
|
mock_new_invoice.return_value = (True,
|
|
new_invoice(1, InvoiceStatus.pending,
|
|
bidding.get_min_bid(n_bytes)))
|
|
# Place all orders as the admin
|
|
for i in range(constants.MAX_PAGE_SIZE + 1):
|
|
post_rv = place_order(client, n_bytes, channel=channel, admin=True)
|
|
assert post_rv.status_code == HTTPStatus.OK
|
|
uuid = post_rv.get_json()['uuid']
|
|
db_order = Order.query.filter_by(uuid=uuid).first()
|
|
db_order.status = OrderStatus.transmitting.value if \
|
|
state == 'queued' else OrderStatus[state].value
|
|
db.session.commit()
|
|
|
|
# Get the orders of each channel as a regular user
|
|
for _channel in constants.CHANNELS:
|
|
get_rv = client.get(f'/orders/{state}?channel={_channel}')
|
|
if 'get' in constants.CHANNEL_INFO[_channel].user_permissions:
|
|
assert get_rv.status_code == HTTPStatus.OK
|
|
get_json_resp = get_rv.get_json()
|
|
n_expected_res = constants.PAGE_SIZE if _channel == channel else 0
|
|
assert len(get_json_resp) == n_expected_res
|
|
else:
|
|
assert get_rv.status_code == HTTPStatus.UNAUTHORIZED
|
|
assert_error(get_rv.get_json(), 'ORDER_CHANNEL_UNAUTHORIZED_OP')
|
|
|
|
# Get the orders of each channel as an admin user
|
|
for _channel in constants.CHANNELS:
|
|
get_rv = client.get(f'/admin/orders/{state}?channel={_channel}')
|
|
assert get_rv.status_code == HTTPStatus.OK
|
|
get_json_resp = get_rv.get_json()
|
|
n_expected_res = constants.PAGE_SIZE if _channel == channel else 0
|
|
assert len(get_json_resp) == n_expected_res
|
|
|
|
|
|
@patch('orders.new_invoice')
|
|
def test_get_pending_orders(mock_new_invoice, client):
|
|
# make some orders
|
|
uuid_order1 = generate_test_order(mock_new_invoice, client)['uuid']
|
|
uuid_order2 = generate_test_order(mock_new_invoice, client)['uuid']
|
|
# manipulate order status for testing the GET /orders endpoint
|
|
db_order = Order.query.filter_by(uuid=uuid_order2).first()
|
|
pay_invoice(db_order.invoices[0])
|
|
db.session.commit()
|
|
|
|
sleep(1.0) # to have different created_at
|
|
uuid_order3 = generate_test_order(mock_new_invoice, client)['uuid']
|
|
|
|
get_rv = client.get('/orders/pending')
|
|
assert get_rv.status_code == HTTPStatus.OK
|
|
get_json_resp = get_rv.get_json()
|
|
assert len(get_json_resp) == 2 # paid order should be filtered out
|
|
assert get_json_resp[0]['uuid'] == uuid_order3
|
|
assert get_json_resp[1]['uuid'] == uuid_order1
|
|
|
|
|
|
@patch('orders.new_invoice')
|
|
@patch('constants.PAGE_SIZE', 3) # change PAGE_SIZE to run this test faster
|
|
def test_get_pending_orders_paging(mock_new_invoice, client):
|
|
# make more orders than PAGE_SIZE
|
|
n_orders = constants.PAGE_SIZE + 2
|
|
n_bytes = 500
|
|
mock_new_invoice.return_value = (True,
|
|
new_invoice(1, InvoiceStatus.pending,
|
|
bidding.get_min_bid(n_bytes)))
|
|
order_uuids = []
|
|
for i in range(n_orders):
|
|
post_rv = place_order(client, n_bytes)
|
|
assert post_rv.status_code == HTTPStatus.OK
|
|
order_uuids.append(post_rv.get_json()['uuid'])
|
|
sleep(1.0) # to have different created_at
|
|
|
|
# Check all orders were created
|
|
all_orders = Order.query.filter_by(status=OrderStatus.pending.value).all()
|
|
assert len(all_orders) == n_orders
|
|
|
|
# Fetch the pending orders with paging
|
|
get_rv = client.get('/orders/pending')
|
|
assert get_rv.status_code == HTTPStatus.OK
|
|
get_json_resp = get_rv.get_json()
|
|
|
|
# Only the last PAGE_SIZE orders should be returned
|
|
assert len(get_json_resp) == constants.PAGE_SIZE
|
|
expected_uuids = order_uuids[-constants.PAGE_SIZE:]
|
|
expected_uuids.reverse() # match the sorting from newest to oldest
|
|
for i in range(constants.PAGE_SIZE):
|
|
assert get_json_resp[i]['uuid'] == expected_uuids[i]
|
|
|
|
|
|
@patch('orders.new_invoice')
|
|
def test_get_queued_orders(mock_new_invoice, client):
|
|
# Create some orders with different states
|
|
order = {}
|
|
for state in [
|
|
'pending', 'paid', 'transmitting', 'sent', 'received', 'confirming'
|
|
]:
|
|
order[state] = generate_test_order(mock_new_invoice,
|
|
client,
|
|
order_status=OrderStatus[state])
|
|
|
|
# Request queued orders
|
|
get_rv = client.get('/orders/queued')
|
|
assert get_rv.status_code == HTTPStatus.OK
|
|
queued_uuids = [order['uuid'] for order in get_rv.get_json()]
|
|
|
|
# The expectation is that only paid, transmitting and confirming
|
|
# orders are returned
|
|
assert len(queued_uuids) == 3
|
|
assert order['pending']['uuid'] not in queued_uuids
|
|
assert order['paid']['uuid'] in queued_uuids
|
|
assert order['transmitting']['uuid'] in queued_uuids
|
|
assert order['sent']['uuid'] not in queued_uuids
|
|
assert order['received']['uuid'] not in queued_uuids
|
|
assert order['confirming']['uuid'] in queued_uuids
|
|
|
|
|
|
@patch('orders.new_invoice')
|
|
def test_get_queued_orders_sorting(mock_new_invoice, client):
|
|
n_bytes = 500
|
|
mock_new_invoice.return_value = (True,
|
|
new_invoice(1, InvoiceStatus.pending,
|
|
bidding.get_min_bid(n_bytes)))
|
|
n_orders = 10
|
|
bid_per_byte_map = {}
|
|
for i in range(n_orders):
|
|
post_rv = place_order(client, n_bytes)
|
|
assert post_rv.status_code == HTTPStatus.OK
|
|
order_uuid = post_rv.get_json()['uuid']
|
|
db_order = Order.query.filter_by(uuid=order_uuid).first()
|
|
db_order.status = OrderStatus.transmitting.value
|
|
bid_per_byte = random.randint(1, 10000)
|
|
bid_per_byte_map[order_uuid] = bid_per_byte
|
|
db_order.bid_per_byte = bid_per_byte
|
|
db.session.commit()
|
|
|
|
expected_sorted_orders = sorted(bid_per_byte_map.items(),
|
|
key=lambda x: x[1],
|
|
reverse=True)
|
|
|
|
get_rv = client.get('/orders/queued')
|
|
assert get_rv.status_code == HTTPStatus.OK
|
|
get_json_resp = get_rv.get_json()
|
|
assert len(get_json_resp) == n_orders
|
|
for i in range(n_orders):
|
|
assert get_json_resp[i]['uuid'] == expected_sorted_orders[i][0]
|
|
assert get_json_resp[i]['bid_per_byte'] == expected_sorted_orders[i][1]
|
|
|
|
|
|
@patch('orders.new_invoice')
|
|
def test_get_sent_orders(mock_new_invoice, client):
|
|
# Create some orders with different states
|
|
order = {}
|
|
for state in ['pending', 'transmitting', 'sent', 'received']:
|
|
order[state] = generate_test_order(mock_new_invoice,
|
|
client,
|
|
order_status=OrderStatus[state])
|
|
|
|
# Request sent orders
|
|
get_rv = client.get('/orders/sent')
|
|
assert get_rv.status_code == HTTPStatus.OK
|
|
get_json_resp = get_rv.get_json()
|
|
sent_uuids = [order['uuid'] for order in get_rv.get_json()]
|
|
|
|
# The expectation is that only sent and received orders are returned
|
|
assert len(get_json_resp) == 2 # pending order should be filtered
|
|
assert order['pending']['uuid'] not in sent_uuids
|
|
assert order['transmitting']['uuid'] not in sent_uuids
|
|
assert order['sent']['uuid'] in sent_uuids
|
|
assert order['received']['uuid'] in sent_uuids
|
|
|
|
|
|
@patch('orders.new_invoice')
|
|
def test_get_sent_orders_sorting(mock_new_invoice, client):
|
|
n_bytes = 500
|
|
mock_new_invoice.return_value = (True,
|
|
new_invoice(1, InvoiceStatus.pending,
|
|
bidding.get_min_bid(n_bytes)))
|
|
order_uuids = []
|
|
for i in range(5):
|
|
post_rv = place_order(client, n_bytes)
|
|
assert post_rv.status_code == HTTPStatus.OK
|
|
post_json_resp = post_rv.get_json()
|
|
order_uuid = post_json_resp['uuid']
|
|
order_uuids.append(order_uuid)
|
|
db_order = Order.query.filter_by(uuid=order_uuid).first()
|
|
db_order.status = OrderStatus.sent.value
|
|
order_created_at = db_order.created_at
|
|
db_order.ended_transmission_at = order_created_at + timedelta(0, 100)
|
|
db.session.commit()
|
|
sleep(1.0) # to have different created_at
|
|
|
|
order_uuids.reverse() # expected uuid order in the response
|
|
get_rv = client.get('/orders/sent')
|
|
assert get_rv.status_code == HTTPStatus.OK
|
|
get_json_resp = get_rv.get_json()
|
|
assert len(get_json_resp) == 5
|
|
for i in range(5):
|
|
assert get_json_resp[i]['uuid'] == order_uuids[i]
|
|
|
|
|
|
@patch('orders.new_invoice')
|
|
@patch('constants.PAGE_SIZE', 3) # change PAGE_SIZE to run this test faster
|
|
def test_get_sent_orders_paging(mock_new_invoice, client):
|
|
# make more orders than PAGE_SIZE
|
|
n_orders = constants.PAGE_SIZE + 2
|
|
n_bytes = 500
|
|
mock_new_invoice.return_value = (True,
|
|
new_invoice(1, InvoiceStatus.pending,
|
|
bidding.get_min_bid(n_bytes)))
|
|
order_uuids = []
|
|
for i in range(n_orders):
|
|
post_rv = place_order(client, n_bytes)
|
|
assert post_rv.status_code == HTTPStatus.OK
|
|
order_uuid = post_rv.get_json()['uuid']
|
|
order_uuids.append(order_uuid)
|
|
db_order = Order.query.filter_by(uuid=order_uuid).first()
|
|
db_order.status = OrderStatus.received.value
|
|
order_created_at = db_order.created_at
|
|
db_order.ended_transmission_at = order_created_at + timedelta(0, 100)
|
|
db.session.commit()
|
|
sleep(1.0) # to have different created_at
|
|
|
|
# Check all orders were created
|
|
all_orders = Order.query.filter_by(status=OrderStatus.received.value).all()
|
|
assert len(all_orders) == n_orders
|
|
|
|
# Fetch the sent orders with paging
|
|
get_rv = client.get('/orders/sent')
|
|
assert get_rv.status_code == HTTPStatus.OK
|
|
get_json_resp = get_rv.get_json()
|
|
|
|
# Only the last PAGE_SIZE orders should be returned
|
|
assert len(get_json_resp) == constants.PAGE_SIZE
|
|
expected_uuids = order_uuids[-constants.PAGE_SIZE:]
|
|
expected_uuids.reverse() # match the sorting from newest to oldest
|
|
for i in range(constants.PAGE_SIZE):
|
|
assert get_json_resp[i]['uuid'] == expected_uuids[i]
|
|
|
|
|
|
@patch('orders.new_invoice')
|
|
@patch('constants.FORCE_PAYMENT', True)
|
|
def test_create_order_with_force_payment_enabled(mock_new_invoice, client):
|
|
uuid_order1 = generate_test_order(mock_new_invoice, client)['uuid']
|
|
uuid_order2 = generate_test_order(mock_new_invoice, client)['uuid']
|
|
|
|
db_order1 = Order.query.filter_by(uuid=uuid_order1).first()
|
|
db_invoice1 = db_order1.invoices[0]
|
|
|
|
db_order2 = Order.query.filter_by(uuid=uuid_order2).first()
|
|
db_invoice2 = db_order2.invoices[0]
|
|
|
|
# Since FORCE_PAYMENT is set and both orders have only one invoice, both
|
|
# orders change their statuses to paid. Furthermore, the payment triggers a
|
|
# Tx start verification and, since the transmit queue is empty, order1
|
|
# immediately changes to transmitting state. In contrast, order2 stays in
|
|
# paid state as it needs to wait until the transmission of order1 finishes.
|
|
assert db_order1.status == OrderStatus.transmitting.value
|
|
assert db_invoice1.status == InvoiceStatus.paid.value
|
|
assert db_invoice1.paid_at is not None
|
|
|
|
assert db_order2.status == OrderStatus.paid.value
|
|
assert db_invoice2.status == InvoiceStatus.paid.value
|
|
assert db_invoice2.paid_at is not None
|