blockstream-satellite-api/server/tests/test_order.py

1160 lines
46 KiB
Python
Raw Normal View History

import io
import os
import pytest
from http import HTTPStatus
from unittest.mock import patch
from uuid import uuid4
from constants import InvoiceStatus, OrderStatus
from database import db
from error import assert_error, get_http_error_resp
from models import Invoice, Order, RxConfirmation
from order_helpers import adjust_bids, _paid_invoices_total,\
_unpaid_invoices_total
from regions import Regions, SATELLITE_REGIONS
from utils import hmac_sha256_digest
import bidding
import constants
import server
from common import check_invoice, pay_invoice, check_upload, new_invoice,\
place_order, generate_test_order, rnd_string, upload_test_file
@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)
def check_received_message(order_uuid, received_message):
path = os.path.join(constants.MSG_STORE_PATH, order_uuid)
assert os.path.exists(path)
with open(path, 'rb') as fd:
sent_message = fd.read()
assert sent_message == received_message
@patch('orders.new_invoice')
def test_file_upload(mock_new_invoice, client):
n_bytes = 500
bid = bidding.get_min_bid(n_bytes)
msg = rnd_string(n_bytes)
mock_new_invoice.return_value = (True,
new_invoice(1, InvoiceStatus.pending,
bid))
rv = upload_test_file(client, msg, bid)
assert rv.status_code == HTTPStatus.OK
check_upload(rv.get_json()['uuid'], msg)
check_invoice(rv.get_json()['lightning_invoice'], rv.get_json()['uuid'])
@patch('orders.new_invoice')
def test_text_msg_upload(mock_new_invoice, client):
n_bytes = 500
bid = bidding.get_min_bid(n_bytes)
msg = rnd_string(n_bytes)
mock_new_invoice.return_value = (True,
new_invoice(1, InvoiceStatus.pending,
bid))
rv = client.post('/order', data={'bid': bid, 'message': msg.encode()})
assert rv.status_code == HTTPStatus.OK
check_upload(rv.get_json()['uuid'], msg)
check_invoice(rv.get_json()['lightning_invoice'], rv.get_json()['uuid'])
def test_uploaded_file_too_small(client):
n_bytes = 0
rv = place_order(client, n_bytes)
assert_error(rv.get_json(), 'MESSAGE_FILE_TOO_SMALL')
assert rv.status_code == HTTPStatus.BAD_REQUEST
# Patch the max sizes on each channel to run the test faster
@patch(
'constants.CHANNEL_INFO', {
constants.USER_CHANNEL:
constants.ChannelInfo('transmissions', ['get', 'post', 'delete'], 1000,
1000),
constants.BTC_SRC_CHANNEL:
constants.ChannelInfo('btc-src', ['get'], 500, 1500),
})
def test_uploaded_file_too_large(client):
max_size_user_chan = 1000
max_size_btc_src_chan = 1500
n_bytes = max_size_user_chan + 1
rv = place_order(client, n_bytes)
assert_error(rv.get_json(), 'MESSAGE_FILE_TOO_LARGE')
assert rv.status_code == HTTPStatus.REQUEST_ENTITY_TOO_LARGE
# The limit is different per channel. For instance, the above size should
# work on the btc-src channel.
rv = place_order(client,
n_bytes,
channel=constants.BTC_SRC_CHANNEL,
admin=True)
assert rv.status_code == HTTPStatus.OK
n_bytes = max_size_btc_src_chan + 1
rv = place_order(client, n_bytes)
assert_error(rv.get_json(), 'MESSAGE_FILE_TOO_LARGE')
assert rv.status_code == HTTPStatus.REQUEST_ENTITY_TOO_LARGE
@patch('orders.new_invoice')
def test_uploaded_file_max_size(mock_new_invoice, client):
n_bytes = constants.DEFAULT_MAX_MESSAGE_SIZE
mock_new_invoice.return_value = (True,
new_invoice(1, InvoiceStatus.pending,
bidding.get_min_bid(n_bytes)))
rv = place_order(client, n_bytes)
assert rv.status_code == HTTPStatus.OK
def test_uploaded_text_msg_too_large(client):
n_bytes = constants.MAX_TEXT_MSG_LEN + 1
bid = bidding.get_min_bid(n_bytes)
msg = rnd_string(n_bytes)
rv = client.post('/order', data={'bid': bid, 'message': msg.encode()})
assert rv.status_code == HTTPStatus.BAD_REQUEST
Support multiple parallel logical message channels 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.
2023-01-31 18:14:55 -03:00
def test_post_order_invalid_channel(client):
n_bytes = 10
bid = bidding.get_min_bid(n_bytes)
msg = rnd_string(n_bytes)
rv = client.post('/order',
data={
'bid': bid,
'message': msg.encode(),
'channel': 10
})
assert rv.status_code == HTTPStatus.BAD_REQUEST
assert 'channel' in rv.get_json()
assert "Must be one of" in rv.get_json()['channel'][0]
@patch('orders.new_invoice')
def test_uploaded_text_msg_max_size(mock_new_invoice, client):
n_bytes = constants.MAX_TEXT_MSG_LEN
bid = bidding.get_min_bid(n_bytes)
mock_new_invoice.return_value = (True,
new_invoice(1, InvoiceStatus.pending,
bid))
bid = bidding.get_min_bid(n_bytes)
msg = rnd_string(n_bytes)
rv = client.post('/order', data={'bid': bid, 'message': msg.encode()})
assert rv.status_code == HTTPStatus.OK
@patch('orders.new_invoice')
def test_both_msg_and_file_provided(mock_new_invoice, client):
n_bytes = 500
bid = bidding.get_min_bid(n_bytes)
msg = rnd_string(n_bytes)
mock_new_invoice.return_value = (True,
new_invoice(1, InvoiceStatus.pending,
bid))
rv = client.post('/order',
data={
'bid': bid,
'message': msg.encode(),
'file': (io.BytesIO(msg.encode()), 'testfile')
})
assert rv.status_code == HTTPStatus.BAD_REQUEST
def test_negative_bid(client):
n_bytes = 500
bid = -1
msg = rnd_string(n_bytes)
rv = upload_test_file(client, msg, bid)
assert 'bid' in rv.get_json()
assert rv.get_json()['bid'][0] == 'Must be greater than or equal to 0.'
assert rv.status_code == HTTPStatus.BAD_REQUEST
def test_bid_too_low(client):
n_bytes = 1000
bid = 1051
msg = rnd_string(n_bytes)
rv = upload_test_file(client, msg, bid)
assert_error(rv.get_json(), 'BID_TOO_SMALL')
assert rv.status_code == HTTPStatus.BAD_REQUEST
def test_order_without_message(client):
n_bytes = 500
bid = bidding.get_min_bid(n_bytes)
rv = client.post('/order', data={'bid': bid})
assert_error(rv.get_json(), 'MESSAGE_MISSING')
assert rv.status_code == HTTPStatus.BAD_REQUEST
@patch('orders.new_invoice')
def test_invoice_generation_failure(mock_new_invoice, client):
mock_new_invoice.return_value =\
(False,
get_http_error_resp('LIGHTNING_CHARGE_INVOICE_ERROR'))
n_bytes = 500
rv = place_order(client, n_bytes)
assert rv.status_code == HTTPStatus.BAD_REQUEST
mock_new_invoice.return_value =\
(False,
get_http_error_resp(
'LIGHTNING_CHARGE_WEBHOOK_REGISTRATION_ERROR'
))
rv = place_order(client, n_bytes)
assert rv.status_code == HTTPStatus.BAD_REQUEST
@patch('orders.new_invoice')
def test_get_order(mock_new_invoice, client):
json_response = generate_test_order(mock_new_invoice, client)
uuid = json_response['uuid']
Support multiple parallel logical message channels 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.
2023-01-31 18:14:55 -03:00
auth_token = json_response['auth_token']
Support multiple parallel logical message channels 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.
2023-01-31 18:14:55 -03:00
get_rv = client.get(f'/order/{uuid}', headers={'X-Auth-Token': auth_token})
get_json_resp = get_rv.get_json()
assert get_rv.status_code == HTTPStatus.OK
assert get_json_resp['uuid'] == uuid
@patch('orders.new_invoice')
def test_get_order_auth_token_as_form_param(mock_new_invoice, client):
json_response = generate_test_order(mock_new_invoice, client)
uuid = json_response['uuid']
Support multiple parallel logical message channels 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.
2023-01-31 18:14:55 -03:00
auth_token = json_response['auth_token']
Support multiple parallel logical message channels 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.
2023-01-31 18:14:55 -03:00
get_rv = client.get(f'/order/{uuid}', data={'auth_token': auth_token})
get_json_resp = get_rv.get_json()
assert get_rv.status_code == HTTPStatus.OK
assert get_json_resp['uuid'] == uuid
@patch('orders.new_invoice')
def test_get_order_auth_token_as_query_param(mock_new_invoice, client):
json_response = generate_test_order(mock_new_invoice, client)
uuid = json_response['uuid']
auth_token = json_response['auth_token']
Support multiple parallel logical message channels 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.
2023-01-31 18:14:55 -03:00
get_rv = client.get(f'/order/{uuid}?auth_token={auth_token}')
get_json_resp = get_rv.get_json()
assert get_rv.status_code == HTTPStatus.OK
assert get_json_resp['uuid'] == uuid
def test_get_nonexistent_order(client):
uuid = str(uuid4())
rv = client.get(f'/order/{uuid}',
headers={'X-Auth-Token': 'test-auth-token'})
assert_error(rv.get_json(), 'ORDER_NOT_FOUND')
assert rv.status_code == HTTPStatus.NOT_FOUND
@patch('orders.new_invoice')
def test_get_order_wrong_auth_token(mock_new_invoice, client):
uuid = generate_test_order(mock_new_invoice, client)['uuid']
get_rv = client.get(f'/order/{uuid}',
headers={'X-Auth-Token': 'wrong-auth-token'})
assert_error(get_rv.get_json(), 'INVALID_AUTH_TOKEN')
assert get_rv.status_code == HTTPStatus.UNAUTHORIZED
@patch('orders.new_invoice')
def test_get_order_missing_auth_token(mock_new_invoice, client):
uuid = generate_test_order(mock_new_invoice, client)['uuid']
get_rv = client.get(f'/order/{uuid}')
assert_error(get_rv.get_json(), 'INVALID_AUTH_TOKEN')
assert get_rv.status_code == HTTPStatus.UNAUTHORIZED
Support multiple parallel logical message channels 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.
2023-01-31 18:14:55 -03:00
@patch('orders.new_invoice')
def test_get_admin_order(mock_new_invoice, client):
# Post order on a channel that forbids users from fetching messages
json_response = generate_test_order(mock_new_invoice,
client,
channel=constants.AUTH_CHANNEL,
admin=True)
uuid = json_response['uuid']
auth_token = json_response['auth_token']
# Getting through the normal route should fail
get_rv = client.get(f'/order/{uuid}', headers={'X-Auth-Token': auth_token})
assert get_rv.status_code == HTTPStatus.UNAUTHORIZED
assert_error(get_rv.get_json(), 'ORDER_CHANNEL_UNAUTHORIZED_OP')
# Getting through the admin route should work
get_rv = client.get(f'/admin/order/{uuid}',
headers={'X-Auth-Token': auth_token})
get_json_resp = get_rv.get_json()
assert get_rv.status_code == HTTPStatus.OK
assert get_json_resp['uuid'] == uuid
def test_adjust_bids(client):
# if the values like bid, unpaid_bid, bid_per_byte are wrong or become
# obsolete due to changes in the invoices, the adjust_bids function should
# be able to fix them all
n_bytes = 1000
paid_bid = 1000
unpaid_bid = 10000
order = Order(id=1,
uuid='a-b-c',
bid=123,
message_size=n_bytes,
bid_per_byte=56.0,
message_digest='abcd',
status=OrderStatus.pending.value,
unpaid_bid=2345,
invoices=[
new_invoice(1, InvoiceStatus.pending,
int(0.4 * unpaid_bid)),
new_invoice(1, InvoiceStatus.pending,
int(0.6 * unpaid_bid)),
new_invoice(1, InvoiceStatus.paid, int(0.3 * paid_bid)),
new_invoice(1, InvoiceStatus.paid, int(0.7 * paid_bid))
])
assert _paid_invoices_total(order) == paid_bid
assert _unpaid_invoices_total(order) == unpaid_bid
adjust_bids(order)
assert order.bid == paid_bid
assert order.unpaid_bid == unpaid_bid
expected_bid_per_byte = paid_bid / (n_bytes + 52) # w/ 52 overhead bytes
assert order.bid_per_byte == expected_bid_per_byte
@patch('orders.new_invoice')
def test_negative_bid_increase_error(mock_new_invoice, client):
initial_bid = 1000
json_response = generate_test_order(mock_new_invoice,
client,
bid=initial_bid)
uuid = json_response['uuid']
auth_token = json_response['auth_token']
# Bump the bid with a negative value
bump_rv = client.post(f'/order/{uuid}/bump',
data={
'bid_increase': -1,
},
headers={'X-Auth-Token': auth_token})
assert bump_rv.status_code == HTTPStatus.BAD_REQUEST
@patch('orders.new_invoice')
def test_bump_order(mock_new_invoice, client):
initial_bid = 1000
json_response = generate_test_order(mock_new_invoice,
client,
bid=initial_bid)
uuid = json_response['uuid']
auth_token = json_response['auth_token']
get_rv = client.get(f'/order/{uuid}', headers={'X-Auth-Token': auth_token})
assert get_rv.status_code == HTTPStatus.OK
get_json_resp = get_rv.get_json()
assert get_json_resp['unpaid_bid'] == initial_bid
db_order = Order.query.filter_by(uuid=uuid).first()
assert len(db_order.invoices) == 1
# Bump the bid on the existing order
bid_increase = 2500
mock_new_invoice.return_value = (True,
new_invoice(1, InvoiceStatus.pending,
bid_increase))
bump_rv = client.post(f'/order/{uuid}/bump',
data={
'bid_increase': bid_increase,
},
headers={'X-Auth-Token': auth_token})
assert bump_rv.status_code == HTTPStatus.OK
# Get the order and check if the bid was bumped successfully
get_rv = client.get(f'/order/{uuid}', headers={'X-Auth-Token': auth_token})
assert get_rv.status_code == HTTPStatus.OK
get_json_resp = get_rv.get_json()
assert get_json_resp['unpaid_bid'] == initial_bid + bid_increase
db_order = Order.query.filter_by(uuid=uuid).first()
assert len(db_order.invoices) == 2
# If a bump request fails, the order should stay untouched
second_bid_increase = 3000
mock_new_invoice.return_value = \
(False,
get_http_error_resp('LIGHTNING_CHARGE_INVOICE_ERROR'))
bump_rv = client.post(f'/order/{uuid}/bump',
data={
'bid_increase': second_bid_increase,
},
headers={'X-Auth-Token': auth_token})
assert bump_rv.status_code == HTTPStatus.BAD_REQUEST
# Get the order and verify the second bid increase (which failed) was
# completely ignored
get_rv = client.get(f'/order/{uuid}', headers={'X-Auth-Token': auth_token})
assert get_rv.status_code == HTTPStatus.OK
get_json_resp = get_rv.get_json()
assert get_json_resp['unpaid_bid'] == initial_bid + bid_increase
db_order = Order.query.filter_by(uuid=uuid).first()
assert len(db_order.invoices) == 2
@patch('orders.new_invoice')
def test_bump_transmitted_order(mock_new_invoice, client):
initial_bid = 1000
json_response = generate_test_order(mock_new_invoice,
client,
bid=initial_bid)
uuid = json_response['uuid']
auth_token = json_response['auth_token']
db_order = Order.query.filter_by(uuid=uuid).first()
pay_invoice(db_order.invoices[0], client)
db.session.commit()
get_rv = client.get(f'/order/{uuid}', headers={'X-Auth-Token': auth_token})
assert get_rv.status_code == HTTPStatus.OK
get_json_resp = get_rv.get_json()
assert get_json_resp['unpaid_bid'] == 0
assert get_json_resp['bid'] == initial_bid
assert get_json_resp['status'] == OrderStatus.transmitting.name
# Since the order is already in transmitting state, a bump request should
# return error
bid_increase = 2500
mock_new_invoice.return_value = (True,
new_invoice(1, InvoiceStatus.pending,
bid_increase))
bump_rv = client.post(f'/order/{uuid}/bump',
data={
'bid_increase': bid_increase,
},
headers={'X-Auth-Token': auth_token})
assert bump_rv.status_code == HTTPStatus.BAD_REQUEST
assert_error(bump_rv.get_json(), 'ORDER_BUMP_ERROR')
Support multiple parallel logical message channels 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.
2023-01-31 18:14:55 -03:00
@patch('orders.new_invoice')
def test_bump_non_paid_order(mock_new_invoice, client):
# Send order over the gossip channel, which is not paid.
initial_bid = 1000
json_response = generate_test_order(mock_new_invoice,
client,
bid=initial_bid,
channel=constants.GOSSIP_CHANNEL,
admin=True)
uuid = json_response['uuid']
auth_token = json_response['auth_token']
# Since the order is not paid, a bump request should not be authorized
bid_increase = 2500
bump_rv = client.post(f'/order/{uuid}/bump',
data={
'bid_increase': bid_increase,
},
headers={'X-Auth-Token': auth_token})
assert bump_rv.status_code == HTTPStatus.UNAUTHORIZED
assert_error(bump_rv.get_json(), 'ORDER_CHANNEL_UNAUTHORIZED_OP')
@patch('orders.new_invoice')
def test_cancel_order(mock_new_invoice, client):
bid = 1000
json_response = generate_test_order(mock_new_invoice, client, bid=bid)
uuid = json_response['uuid']
auth_token = json_response['auth_token']
db_order = Order.query.filter_by(uuid=uuid).first()
assert db_order.status == OrderStatus.pending.value
assert db_order.cancelled_at is None
# Only pending and paid orders can be cancelled
for status in OrderStatus:
db_order.status = status.value
db.session.commit()
if status in [OrderStatus.paid, OrderStatus.pending]:
delete_rv = client.delete(f'/order/{uuid}',
headers={'X-Auth-Token': auth_token})
assert delete_rv.status_code == HTTPStatus.OK
else:
delete_rv = client.delete(f'/order/{uuid}',
headers={'X-Auth-Token': auth_token})
assert delete_rv.status_code == HTTPStatus.BAD_REQUEST
assert_error(delete_rv.get_json(), 'ORDER_CANCELLATION_ERROR')
@patch('orders.new_invoice')
def test_cancel_order_twice(mock_new_invoice, client):
bid = 1000
json_response = generate_test_order(mock_new_invoice, client, bid=bid)
uuid = json_response['uuid']
auth_token = json_response['auth_token']
db_order = Order.query.filter_by(uuid=uuid).first()
assert db_order.status == OrderStatus.pending.value
assert db_order.cancelled_at is None
message_path = os.path.join(constants.MSG_STORE_PATH, uuid)
assert os.path.exists(message_path)
# Cancel the order
delete_rv = client.delete(f'/order/{uuid}',
headers={'X-Auth-Token': auth_token})
assert delete_rv.status_code == HTTPStatus.OK
db_order = Order.query.filter_by(uuid=uuid).first()
assert db_order.status == OrderStatus.cancelled.value
assert db_order.cancelled_at is not None
assert not os.path.exists(message_path)
# Try to cancel the order again
delete_rv = client.delete(f'/order/{uuid}',
headers={'X-Auth-Token': auth_token})
assert delete_rv.status_code == HTTPStatus.BAD_REQUEST
assert_error(delete_rv.get_json(), 'ORDER_CANCELLATION_ERROR')
def test_cancel_non_existing_order(client):
delete_rv = client.delete('/order/13245',
headers={'X-Auth-Token': 'token'})
assert delete_rv.status_code == HTTPStatus.NOT_FOUND
assert_error(delete_rv.get_json(), 'ORDER_NOT_FOUND')
Support multiple parallel logical message channels 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.
2023-01-31 18:14:55 -03:00
@patch('orders.new_invoice')
def test_cancel_order_unauthorized_channel_op(mock_new_invoice, client):
# Place order on a channel that forbids uses from deleting orders.
bid = 1000
json_response = generate_test_order(mock_new_invoice,
client,
bid=bid,
channel=constants.GOSSIP_CHANNEL,
admin=True)
uuid = json_response['uuid']
auth_token = json_response['auth_token']
# Deleting through the regular user endpoint should fail
delete_rv = client.delete(f'/order/{uuid}',
headers={'X-Auth-Token': auth_token})
assert delete_rv.status_code == HTTPStatus.UNAUTHORIZED
assert_error(delete_rv.get_json(), 'ORDER_CHANNEL_UNAUTHORIZED_OP')
# Deleting through the admin endpoint should work (i.e., won't return
# unauthorized operation)
delete_rv = client.delete(f'/admin/order/{uuid}',
headers={'X-Auth-Token': auth_token})
# In this case, it hits a cancellation error because the order is already
# in transmitting state, given that the gossip channel is auto-paid.
assert delete_rv.status_code == HTTPStatus.BAD_REQUEST
assert_error(delete_rv.get_json(), 'ORDER_CANCELLATION_ERROR')
db_order = Order.query.filter_by(uuid=uuid).first()
assert db_order.status == OrderStatus.transmitting.value
assert db_order.cancelled_at is None
def test_get_sent_message_for_nonexisting_uuid(client):
# Try to get message for a non existing uuid
rv = client.get('/order/some-uuid/sent_message')
assert rv.status_code == HTTPStatus.NOT_FOUND
assert_error(rv.get_json(), 'ORDER_NOT_FOUND')
@patch('orders.new_invoice')
def test_get_sent_message_for_pending_order(mock_new_invoice, client):
uuid = generate_test_order(mock_new_invoice, client)['uuid']
# The test order has pending status. Hence, fetching via the
# /order/{uuid}/sent_message endpoint should fail. Only "sent" and
# "transmitting" orders can be requested.
rv = client.get(f'/order/{uuid}/sent_message')
assert rv.status_code == HTTPStatus.NOT_FOUND
assert_error(rv.get_json(), 'ORDER_NOT_FOUND')
@patch('orders.new_invoice')
@pytest.mark.parametrize("status",
[OrderStatus.sent, OrderStatus.transmitting])
def test_get_sent_message_from_uuid(mock_new_invoice, client, status):
uuid = generate_test_order(mock_new_invoice, client,
order_status=status)['uuid']
rv = client.get(f'/order/{uuid}/sent_message')
assert rv.status_code == HTTPStatus.OK
received_message = rv.data
check_received_message(uuid, received_message)
def test_get_sent_message_for_nonexisting_seq_number(client):
# Try to get message for a non existing seq number
rv = client.get('/message/1')
assert rv.status_code == HTTPStatus.NOT_FOUND
assert_error(rv.get_json(), 'SEQUENCE_NUMBER_NOT_FOUND')
@patch('orders.new_invoice')
def test_get_sent_message_by_seq_number_for_paid_order(mock_new_invoice,
client):
uuid = generate_test_order(mock_new_invoice, client)['uuid']
db_order = Order.query.filter_by(uuid=uuid).first()
pay_invoice(db_order.invoices[0], client)
db.session.commit()
# Because the invoice was paid, the order should go immediately into
# transmitting state and be assigned with sequence number 1.
rv = client.get('/message/1')
assert rv.status_code == HTTPStatus.OK
received_message = rv.data
check_received_message(uuid, received_message)
@patch('orders.new_invoice')
@pytest.mark.parametrize(
"status",
[OrderStatus.sent, OrderStatus.received, OrderStatus.transmitting])
def test_get_sent_message_by_seq_number(mock_new_invoice, client, status):
uuid = generate_test_order(mock_new_invoice,
client,
order_status=status,
tx_seq_num=1)['uuid']
# Get order's sent message by seq number
rv = client.get('/message/1')
assert rv.status_code == HTTPStatus.OK
received_message = rv.data
check_received_message(uuid, received_message)
Support multiple parallel logical message channels 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.
2023-01-31 18:14:55 -03:00
@patch('orders.new_invoice')
def test_get_sent_message_by_seq_number_unauthorized_channel_op(
mock_new_invoice, client):
# Create an order on the auth channel, which forbids GET requests from
# users. Make sure those requests fail. And use the /admin/order endpoint
# when POSTing the order.
uuid = generate_test_order(mock_new_invoice,
client,
order_status=OrderStatus.sent,
tx_seq_num=1,
channel=constants.AUTH_CHANNEL,
admin=True)['uuid']
# Reading by sequence number via the regular route should fail
rv = client.get('/message/1')
assert rv.status_code == HTTPStatus.UNAUTHORIZED
assert_error(rv.get_json(), 'ORDER_CHANNEL_UNAUTHORIZED_OP')
# Reading by sequence number via the admin route should fail
rv = client.get('/admin/message/1')
assert rv.status_code == HTTPStatus.OK
received_message = rv.data
check_received_message(uuid, received_message)
@patch('orders.new_invoice')
@pytest.mark.parametrize("channel", constants.CHANNELS)
def test_get_sent_message_admin(mock_new_invoice, client, channel):
# the admin should be able to get messages from any channel
uuid = generate_test_order(mock_new_invoice,
client,
order_status=OrderStatus.sent,
tx_seq_num=1,
channel=channel,
admin=True)['uuid']
rv = client.get(f'/admin/message/1?channel={channel}')
assert rv.status_code == HTTPStatus.OK
received_message = rv.data
check_received_message(uuid, received_message)
@patch('orders.new_invoice')
@pytest.mark.parametrize("channel", [
constants.GOSSIP_CHANNEL, constants.BTC_SRC_CHANNEL, constants.AUTH_CHANNEL
])
def test_post_order_unauthorized_channel(mock_new_invoice, client, channel):
# users are not authorized to post to some channels
n_bytes = 500
bid = bidding.get_min_bid(n_bytes)
msg = rnd_string(n_bytes)
mock_new_invoice.return_value = (True,
new_invoice(1, InvoiceStatus.pending,
bid))
rv = client.post('/order',
data={
'bid': bid,
'message': msg.encode(),
'channel': channel
})
assert rv.status_code == HTTPStatus.UNAUTHORIZED
assert_error(rv.get_json(), 'ORDER_CHANNEL_UNAUTHORIZED_OP')
@patch('orders.new_invoice')
@pytest.mark.parametrize("channel", constants.CHANNELS)
def test_post_order_admin(mock_new_invoice, client, channel):
# the admin should be allowed to POST messages to any channel
n_bytes = 500
bid = bidding.get_min_bid(n_bytes)
msg = rnd_string(n_bytes)
mock_new_invoice.return_value = (True,
new_invoice(1, InvoiceStatus.pending,
bid))
rv = client.post('/admin/order',
data={
'bid': bid,
'message': msg.encode(),
'channel': channel
})
assert rv.status_code == HTTPStatus.OK
check_upload(rv.get_json()['uuid'], msg)
@patch('orders.new_invoice')
def test_confirm_tx_missing_or_invalid_param(mock_new_invoice, client):
post_rv = client.post('/order/tx/1')
assert post_rv.status_code == HTTPStatus.BAD_REQUEST
post_rv = client.post('/order/tx/1', data={'regions': 'a'})
assert post_rv.status_code == HTTPStatus.BAD_REQUEST
post_rv = client.post('/order/tx/1', data={'regions': 1})
assert post_rv.status_code == HTTPStatus.BAD_REQUEST
# Confirm tx of a non existing sequence number
post_rv = client.post('/order/tx/2', data={"regions": [[1]]})
assert post_rv.status_code == HTTPStatus.NOT_FOUND
assert_error(post_rv.get_json(), 'SEQUENCE_NUMBER_NOT_FOUND')
# Create a test order but confirm Tx for an invalid region
generate_test_order(mock_new_invoice, client, tx_seq_num=1)
post_rv = client.post('/order/tx/1', data={'regions': [[20, 1]]})
assert post_rv.status_code == HTTPStatus.BAD_REQUEST
@patch('orders.new_invoice')
def test_confirm_tx(mock_new_invoice, client):
uuid = generate_test_order(mock_new_invoice, client, tx_seq_num=1)['uuid']
db_order = Order.query.filter_by(uuid=uuid).first()
# Confirm tx for a single region
post_rv = client.post('/order/tx/1',
data={'regions': [[Regions.t11n_afr.value]]})
assert len(db_order.tx_confirmations) == 1
assert db_order.tx_confirmations[0].region_id == SATELLITE_REGIONS[
Regions.t11n_afr]['id']
# Confirm tx for multiple regions
post_rv = client.post(
'/order/tx/1',
data={
'regions':
[[Regions.g18.value, Regions.e113.value, Regions.t11n_afr.value]]
})
assert post_rv.status_code == HTTPStatus.OK
assert len(db_order.tx_confirmations) == 3
assert db_order.tx_confirmations[0].region_id == SATELLITE_REGIONS[
Regions.t11n_afr]['id']
assert db_order.tx_confirmations[1].region_id == SATELLITE_REGIONS[
Regions.g18]['id']
assert db_order.tx_confirmations[2].region_id == SATELLITE_REGIONS[
Regions.e113]['id']
@patch('orders.new_invoice')
def test_confirm_rx_missing_or_invalid_param(mock_new_invoice, client):
post_rv = client.post('/order/rx/1')
assert post_rv.status_code == HTTPStatus.BAD_REQUEST
post_rv = client.post('/order/rx/1', data={'region': 'a'})
assert post_rv.status_code == HTTPStatus.BAD_REQUEST
post_rv = client.post('/order/rx/1', data={'regions': [[1, 2]]})
assert post_rv.status_code == HTTPStatus.BAD_REQUEST
# Confirm rx of a non existing sequence number
post_rv = client.post('/order/rx/2', data={"region": 1})
assert post_rv.status_code == HTTPStatus.NOT_FOUND
assert_error(post_rv.get_json(), 'SEQUENCE_NUMBER_NOT_FOUND')
# Create a test order but confirm Rx for an invalid region
generate_test_order(mock_new_invoice, client, tx_seq_num=1)
post_rv = client.post('/order/rx/1', data={'region': 20})
assert post_rv.status_code == HTTPStatus.BAD_REQUEST
@patch('orders.new_invoice')
def test_confirm_rx(mock_new_invoice, client):
uuid = generate_test_order(mock_new_invoice, client, tx_seq_num=1)['uuid']
db_order = Order.query.filter_by(uuid=uuid).first()
post_rv = client.post('/order/rx/1', data={'region': Regions.g18.value})
assert post_rv.status_code == HTTPStatus.OK
assert len(db_order.rx_confirmations) == 1
assert db_order.rx_confirmations[0].region_id == SATELLITE_REGIONS[
Regions.g18]['id']
@patch('orders.new_invoice')
@pytest.mark.parametrize(
"tx_regions",
[[Regions.g18.value], [Regions.g18.value, Regions.e113.value],
[
Regions.g18.value, Regions.e113.value, Regions.t11n_afr.value,
Regions.t11n_eu.value, Regions.t18v_c.value
],
[
Regions.g18.value, Regions.e113.value, Regions.t11n_afr.value,
Regions.t11n_eu.value, Regions.t18v_c.value, Regions.t18v_c.value
]])
@pytest.mark.parametrize(
"rx_regions",
[[Regions.g18.value], [Regions.g18.value, Regions.e113.value],
[Regions.g18.value, Regions.e113.value, Regions.t18v_c.value],
[
Regions.g18.value, Regions.e113.value, Regions.t18v_c.value,
Regions.t18v_c.value
]])
def test_sent_or_received_criteria_met_inadequate_regions(
mock_new_invoice, client, tx_regions, rx_regions):
uuid = generate_test_order(mock_new_invoice,
client,
order_status=OrderStatus.transmitting,
tx_seq_num=1)['uuid']
# Confirm tx
post_rv = client.post('/order/tx/1', data={'regions': [tx_regions]})
assert post_rv.status_code == HTTPStatus.OK
# Confirm rx
for region in rx_regions:
post_rv = client.post('/order/rx/1', data={'region': region})
assert post_rv.status_code == HTTPStatus.OK
# The order status should not change to sent nor received because not
# enough regions have confirmed tx/rx
db_order = Order.query.filter_by(uuid=uuid).first()
assert db_order.status == OrderStatus.confirming.value
@patch('orders.new_invoice')
def test_sent_or_received_criteria_met_for_unsent_order(
mock_new_invoice, client):
uuid = generate_test_order(mock_new_invoice, client, tx_seq_num=1)['uuid']
# Confirm tx for all 6 regions
post_rv = client.post('/order/tx/1',
data={'regions': [[e.value for e in Regions]]})
assert post_rv.status_code == HTTPStatus.OK
# Confirm rx for all regions except africa and europe
for region in [
Regions.g18.value, Regions.e113.value, Regions.t18v_c.value,
Regions.t18v_ku.value
]:
post_rv = client.post('/order/rx/1', data={'region': region})
assert post_rv.status_code == HTTPStatus.OK
# Although all the required Tx/Rx confirmations are available, the order
# still cannot change to "received" state, as that requires the order to be
# in "sent" state before. The test order is still in "pending" state.
db_order = Order.query.filter_by(uuid=uuid).first()
assert db_order.status == OrderStatus.pending.value
@patch('orders.new_invoice')
def test_sent_or_received_criteria_met_successfully(mock_new_invoice, client):
uuid = generate_test_order(mock_new_invoice,
client,
order_status=OrderStatus.transmitting,
tx_seq_num=1)['uuid']
# Confirm tx for all 6 regions
post_rv = client.post('/order/tx/1',
data={'regions': [[e.value for e in Regions]]})
assert post_rv.status_code == HTTPStatus.OK
# Order's status should change to sent
db_order = Order.query.filter_by(uuid=uuid).first()
assert db_order.status == OrderStatus.sent.value
# Confirm rx for all regions except africa and europe
for region in [
Regions.g18.value, Regions.e113.value, Regions.t18v_c.value,
Regions.t18v_ku.value
]:
post_rv = client.post('/order/rx/1', data={'region': region})
assert post_rv.status_code == HTTPStatus.OK
# Order's status should change to received
db_order = Order.query.filter_by(uuid=uuid).first()
assert db_order.status == OrderStatus.received.value
# Synthesized rx_confirmations for africa and europe should be created
for region in [Regions.t11n_afr, Regions.t11n_eu]:
db_rx_confirmation = RxConfirmation.query.filter_by(
order_id=db_order.id).filter_by(
region_id=SATELLITE_REGIONS[region]['id']).all()
assert len(db_rx_confirmation) == 1
assert db_rx_confirmation[0].presumed
@patch('orders.new_invoice')
def test_confirm_tx_repeated_regions(mock_new_invoice, client):
uuid = generate_test_order(mock_new_invoice, client, tx_seq_num=1)['uuid']
db_order1 = Order.query.filter_by(uuid=uuid).first()
uuid = generate_test_order(mock_new_invoice, client, tx_seq_num=2)['uuid']
db_order2 = Order.query.filter_by(uuid=uuid).first()
# Confirm tx for a single region
post_rv = client.post('/order/tx/1',
data={'regions': [[Regions.t11n_afr.value]]})
assert post_rv.status_code == HTTPStatus.OK
assert len(db_order1.tx_confirmations) == 1
# Re-Confirm tx for the same region
post_rv = client.post('/order/tx/1',
data={'regions': [[Regions.t11n_afr.value]]})
assert post_rv.status_code == HTTPStatus.OK
assert len(db_order1.tx_confirmations) == 1
# Confirm tx for multiple regions, including t11n_afr again
post_rv = client.post(
'/order/tx/1',
data={
'regions':
[[Regions.g18.value, Regions.t18v_c.value, Regions.t11n_afr.value]]
})
assert post_rv.status_code == HTTPStatus.OK
assert len(db_order1.tx_confirmations) == 3
# Confirm tx for multiple regions, different order_id
post_rv = client.post('/order/tx/2',
data={
'regions': [[
Regions.t11n_eu.value, Regions.t18v_ku.value,
Regions.t18v_ku.value
]]
})
assert post_rv.status_code == HTTPStatus.OK
assert len(db_order2.tx_confirmations) == 2
@patch('orders.new_invoice')
def test_confirm_rx_repeated_regions(mock_new_invoice, client):
uuid = generate_test_order(mock_new_invoice, client, tx_seq_num=1)['uuid']
db_order1 = Order.query.filter_by(uuid=uuid).first()
uuid = generate_test_order(mock_new_invoice, client, tx_seq_num=2)['uuid']
db_order2 = Order.query.filter_by(uuid=uuid).first()
# Confirm rx for a region
post_rv = client.post('/order/rx/1', data={'region': Regions.g18.value})
assert post_rv.status_code == HTTPStatus.OK
assert len(db_order1.rx_confirmations) == 1
# Re-Confirm rx for the same region
post_rv = client.post('/order/rx/1', data={'region': Regions.g18.value})
assert post_rv.status_code == HTTPStatus.OK
assert len(db_order1.rx_confirmations) == 1
# Confirm rx for the same region, different order_id
post_rv = client.post('/order/rx/2', data={'region': Regions.g18.value})
assert post_rv.status_code == HTTPStatus.OK
assert len(db_order2.rx_confirmations) == 1
@patch('orders.new_invoice')
@pytest.mark.parametrize("status", [
OrderStatus.transmitting, OrderStatus.sent, OrderStatus.received,
OrderStatus.cancelled, OrderStatus.expired
])
def test_try_to_pay_a_non_pending_order(mock_new_invoice, client, status):
n_bytes = 500
invoice = new_invoice(1, InvoiceStatus.pending,
bidding.get_min_bid(n_bytes))
mock_new_invoice.return_value = (True, invoice)
post_rv = place_order(client, n_bytes)
assert post_rv.status_code == HTTPStatus.OK
uuid_order = post_rv.get_json()['uuid']
db_order = Order.query.filter_by(uuid=uuid_order).first()
db_order.status = status.value
db.session.commit()
charged_auth_token = hmac_sha256_digest(constants.LIGHTNING_WEBHOOK_KEY,
invoice.lid)
rv = client.post(f'/callback/{invoice.lid}/{charged_auth_token}')
assert rv.status_code == HTTPStatus.OK
Support multiple parallel logical message channels 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.
2023-01-31 18:14:55 -03:00
# Refetch the order and the invoice from the database.
# The expectation is that invoice changes its status to paid because
# it had the pending status, but order keeps its current status.
db_invoice = Invoice.query.filter_by(lid=invoice.lid).first()
db_order = Order.query.filter_by(uuid=uuid_order).first()
assert db_invoice.status == InvoiceStatus.paid.value
assert db_order.status == status.value
assert db_invoice.paid_at is not None
@patch('orders.new_invoice')
def test_bump_non_existing_order(mock_new_invoice, client):
bid_increase = 2500
mock_new_invoice.return_value = (True,
new_invoice(1, InvoiceStatus.pending,
bid_increase))
bump_rv = client.post('/order/12345/bump',
data={
'bid_increase': bid_increase,
},
headers={'X-Auth-Token': "non-existing-token"})
assert bump_rv.status_code == HTTPStatus.NOT_FOUND
assert_error(bump_rv.get_json(), 'ORDER_NOT_FOUND')
@patch('orders.new_invoice')
def test_create_order_with_invalid_region(mock_new_invoice, client):
n_bytes = 500
bid = bidding.get_min_bid(n_bytes)
msg = rnd_string(n_bytes)
mock_new_invoice.return_value = (True,
new_invoice(1, InvoiceStatus.pending,
bid))
post_rv = client.post('/order',
data={
'bid': bid,
'message': msg.encode(),
'regions': 'a'
})
assert post_rv.status_code == HTTPStatus.BAD_REQUEST
post_rv = client.post('/order',
data={
'bid': bid,
'message': msg.encode(),
'regions': 1
})
assert post_rv.status_code == HTTPStatus.BAD_REQUEST
post_rv = client.post('/order',
data={
'bid': bid,
'message': msg.encode(),
'regions': [[6, 1]]
})
assert post_rv.status_code == HTTPStatus.BAD_REQUEST
@patch('orders.new_invoice')
def test_sent_or_received_criteria_met_successfully_for_subset_of_regions(
mock_new_invoice, client):
selected_regions = [
Regions.g18.value, Regions.e113.value, Regions.t11n_afr.value
]
uuid = generate_test_order(mock_new_invoice,
client,
tx_seq_num=1,
order_status=OrderStatus.transmitting,
regions=selected_regions)['uuid']
db_order = Order.query.filter_by(uuid=uuid).first()
# Only the selected regions are required to confirm Tx in order for the
# order to change into "sent" state.
post_rv = client.post('/order/tx/1', data={'regions': [selected_regions]})
assert post_rv.status_code == HTTPStatus.OK
assert len(db_order.tx_confirmations) == 3
assert db_order.tx_confirmations[0].region_id == SATELLITE_REGIONS[
Regions.g18]['id']
assert db_order.tx_confirmations[1].region_id == SATELLITE_REGIONS[
Regions.e113]['id']
assert db_order.tx_confirmations[2].region_id == SATELLITE_REGIONS[
Regions.t11n_afr]['id']
db_order = Order.query.filter_by(uuid=uuid).first()
assert db_order.status == OrderStatus.sent.value
# Confirm rx only for the monitored regions in the order request (i.e.,
# excluding t11n_afr)
for region in [Regions.g18.value, Regions.e113.value]:
post_rv = client.post('/order/rx/1', data={'region': region})
assert post_rv.status_code == HTTPStatus.OK
# The order's status should change to received
db_order = Order.query.filter_by(uuid=uuid).first()
assert db_order.status == OrderStatus.received.value
# A synthesized Rx confirmation should be created for t11n_afr
synth_rx_confirmations = RxConfirmation.query.filter_by(
order_id=db_order.id).filter_by(presumed=True).all()
assert len(synth_rx_confirmations) == 1
assert synth_rx_confirmations[0].region_id == SATELLITE_REGIONS[
Regions.t11n_afr]['id']
@patch('orders.new_invoice')
def test_sent_or_received_criteria_met_for_subset_of_presumed_rx_regions(
mock_new_invoice, client):
selected_regions = [Regions.t11n_afr.value, Regions.t11n_eu.value]
uuid = generate_test_order(mock_new_invoice,
client,
tx_seq_num=1,
order_status=OrderStatus.transmitting,
regions=selected_regions)['uuid']
db_order = Order.query.filter_by(uuid=uuid).first()
# Confirm tx only for the two selected regions
post_rv = client.post('/order/tx/1', data={'regions': [selected_regions]})
assert post_rv.status_code == HTTPStatus.OK
# The order status should change to received and NOT sent. The two regions
# in the order request were t11n_afr and t11n_eu. None of these regions
# send Rx confirmations. As a result, the order should automatically move
# from sent to received without any Rx confirmations.
db_order = Order.query.filter_by(uuid=uuid).first()
assert db_order.status == OrderStatus.received.value
# The synthesized Rx confirmations for Africa and Europe should be created
db_rx_confirmation = RxConfirmation.query.filter_by(
order_id=db_order.id).filter_by(presumed=True).all()
assert len(db_rx_confirmation) == 2
assert db_rx_confirmation[0].region_id == SATELLITE_REGIONS[
Regions.t11n_afr]['id']
assert db_rx_confirmation[1].region_id == SATELLITE_REGIONS[
Regions.t11n_eu]['id']
@patch('orders.new_invoice')
def test_sent_or_received_criteria_met_invalid_tx_subset(
mock_new_invoice, client):
selected_regions = [Regions.g18.value, Regions.e113.value]
other_regions = [
Regions.t11n_afr.value, Regions.t11n_eu.value, Regions.t18v_c.value,
Regions.t18v_ku.value
]
uuid = generate_test_order(mock_new_invoice,
client,
tx_seq_num=1,
order_status=OrderStatus.transmitting,
regions=selected_regions)['uuid']
db_order = Order.query.filter_by(uuid=uuid).first()
# Confirm tx over regions other than those explicitly requested
# by the order
post_rv = client.post('/order/tx/1', data={'regions': [other_regions]})
assert post_rv.status_code == HTTPStatus.OK
# The order status should change to "confirming"
db_order = Order.query.filter_by(uuid=uuid).first()
assert db_order.status == OrderStatus.confirming.value
# Confirm tx
post_rv = client.post('/order/tx/1', data={'regions': [selected_regions]})
assert post_rv.status_code == HTTPStatus.OK
# Now the status should get updated to sent
db_order = Order.query.filter_by(uuid=uuid).first()
assert db_order.status == OrderStatus.sent.value
# Confirm rx
for region in other_regions:
post_rv = client.post('/order/rx/1', data={'region': region})
assert post_rv.status_code == HTTPStatus.OK
# The status should stay as sent so long as rx is not confirmed
# by the two regions in the request (g18, e113)
db_order = Order.query.filter_by(uuid=uuid).first()
assert db_order.status == OrderStatus.sent.value
for region in [Regions.g18.value, Regions.e113.value]:
post_rv = client.post('/order/rx/1', data={'region': region})
assert post_rv.status_code == HTTPStatus.OK
# Now the status should change to received
db_order = Order.query.filter_by(uuid=uuid).first()
assert db_order.status == OrderStatus.received.value