blockstream-satellite-api/server/tests/test_order_helpers.py
Blockstream Satellite 5882fddc49 Support retransmission of unconfirmed orders
This change implements a mechanism to retransmit orders if some of the
order's selected regions do not confirm transmission in due time. It
adds a worker to repeatedly check the orders and determine if they need
retransmission. Such orders will be added to a new table named
tx_retries. The tx_start function now first checks if there are regular
new paid orders to transmit. If not, it will check the retransmission
table and retransmit an order from there if one is available.

This patch also introduces a new order state called "retranmission". The
order enters this state while waiting for retransmission.
2021-12-28 10:01:04 -03:00

301 lines
12 KiB
Python

import os
import pytest
from datetime import datetime, timedelta
from http import HTTPStatus
from unittest.mock import patch
from constants import EXPIRE_PENDING_ORDERS_AFTER_DAYS,\
InvoiceStatus, MESSAGE_FILE_RETENTION_TIME_DAYS, \
OrderStatus, MSG_STORE_PATH
from database import db
from models import Order, TxRetry
from regions import Regions, region_number_list_to_code, region_number_to_id
import order_helpers
import server
from common import generate_test_order
@pytest.fixture
def client():
app = server.create_app(from_test=True)
app.app_context().push()
with app.test_client() as client:
yield client
server.teardown_app(app)
@patch('orders.new_invoice')
def test_expire_pending_orders(mock_new_invoice, client):
to_be_expired_order_uuid = generate_test_order(mock_new_invoice,
client)['uuid']
to_be_expired_db_order = Order.query.filter_by(
uuid=to_be_expired_order_uuid).first()
to_be_expired_db_order.created_at = datetime.utcnow() - \
timedelta(days=EXPIRE_PENDING_ORDERS_AFTER_DAYS
+ 1)
db.session.commit()
pending_not_yet_expired_uuid = generate_test_order(mock_new_invoice,
client,
order_id=2)['uuid']
expired_orders = order_helpers.expire_old_pending_orders()
assert len(expired_orders) == 1
assert expired_orders[0].uuid == to_be_expired_order_uuid
# refetch and check
# expectation is that the order gets expired
to_be_expired_db_order = Order.query.filter_by(
uuid=to_be_expired_order_uuid).first()
assert to_be_expired_db_order.status == OrderStatus.expired.value
# The pending order whose expiration time has not been reached yet should
# not get expired
pending_not_yet_expired_db_order = Order.query.filter_by(
uuid=pending_not_yet_expired_uuid).first()
assert pending_not_yet_expired_db_order.status == OrderStatus.pending.value
@patch('orders.new_invoice')
@pytest.mark.parametrize("status", [
OrderStatus.paid, OrderStatus.transmitting, OrderStatus.sent,
OrderStatus.received, OrderStatus.cancelled, OrderStatus.expired
])
def test_expire_non_pending_orders(mock_new_invoice, client, status):
order_uuid = generate_test_order(mock_new_invoice,
client,
order_status=status)['uuid']
db_order = Order.query.filter_by(uuid=order_uuid).first()
db_order.created_at = datetime.utcnow() - \
timedelta(days=EXPIRE_PENDING_ORDERS_AFTER_DAYS
+ 1)
db.session.commit()
order_helpers.expire_old_pending_orders()
# refetch and check
# expectation is that the order's status does not change
db_order = Order.query.filter_by(uuid=order_uuid).first()
assert db_order.status == status.value
@patch('orders.new_invoice')
def test_cleanup_old_message_files(mock_new_invoice, client):
to_be_cleaned_order_uuid = generate_test_order(
mock_new_invoice,
client,
order_id=1,
invoice_status=InvoiceStatus.paid)['uuid']
to_be_cleaned_db_order = Order.query.filter_by(
uuid=to_be_cleaned_order_uuid).first()
to_be_cleaned_db_order.ended_transmission_at = datetime.utcnow() -\
timedelta(days=MESSAGE_FILE_RETENTION_TIME_DAYS
+ 1)
db.session.commit()
not_to_be_cleaned_order_uuid = generate_test_order(
mock_new_invoice,
client,
order_id=1,
invoice_status=InvoiceStatus.paid)['uuid']
not_to_be_cleaned_db_order = Order.query.filter_by(
uuid=not_to_be_cleaned_order_uuid).first()
not_to_be_cleaned_db_order.ended_transmission_at = datetime.utcnow() -\
timedelta(days=MESSAGE_FILE_RETENTION_TIME_DAYS)
db.session.commit()
cleaned_up_orders = order_helpers.cleanup_old_message_files()
assert len(cleaned_up_orders) == 1
assert cleaned_up_orders[0].uuid == to_be_cleaned_order_uuid
# refetch and check
message_path = os.path.join(MSG_STORE_PATH, to_be_cleaned_order_uuid)
assert not os.path.exists(message_path)
message_path = os.path.join(MSG_STORE_PATH, not_to_be_cleaned_order_uuid)
assert os.path.exists(message_path)
@patch('orders.new_invoice')
@pytest.mark.parametrize("status", [
OrderStatus.paid, OrderStatus.transmitting, OrderStatus.sent,
OrderStatus.received, OrderStatus.cancelled, OrderStatus.expired
])
def test_maybe_mark_order_as_expired_for_invalid_order(mock_new_invoice,
client, status):
# when order does not exist
assert order_helpers.maybe_mark_order_as_expired(1) is None
# when order exists, but its status is not pending
generate_test_order(mock_new_invoice,
client,
order_id=1,
order_status=status)
assert order_helpers.maybe_mark_order_as_expired(1) is None
@patch('orders.new_invoice')
def test_maybe_mark_order_as_expired_pending_order_has_pending_invoice(
mock_new_invoice, client):
# when a pending order has a pending invoice
generate_test_order(mock_new_invoice, client, order_id=1)
assert order_helpers.maybe_mark_order_as_expired(1) is None
@patch('orders.new_invoice')
@pytest.mark.parametrize("invoice_status",
[InvoiceStatus.paid, InvoiceStatus.expired])
def test_maybe_mark_order_as_expired_successfully(mock_new_invoice, client,
invoice_status):
# when a pending order does not have any pending invoice
uuid = generate_test_order(mock_new_invoice,
client,
order_id=1,
invoice_status=invoice_status)['uuid']
assert order_helpers.maybe_mark_order_as_expired(1) is not None
db_order = Order.query.filter_by(uuid=uuid).first()
assert db_order.status == OrderStatus.expired.value
@patch('orders.new_invoice')
def test_upsert_retransmission(mock_new_invoice, client, mockredis):
uuid = generate_test_order(mock_new_invoice,
client,
tx_seq_num=1,
order_status=OrderStatus.transmitting,
regions=[
Regions.g18.value, Regions.e113.value,
Regions.t11n_afr.value,
Regions.t11n_eu.value
])['uuid']
db_order = Order.query.filter_by(uuid=uuid).first()
post_rv = client.post(
'/order/tx/1',
data={'regions': [[Regions.g18.value, Regions.e113.value]]})
assert post_rv.status_code == HTTPStatus.OK
db_order = Order.query.filter_by(uuid=uuid).first()
order_helpers.upsert_retransmission(db_order)
# t11n_afr and t11n_eu confirmations are missing
retry_order = TxRetry.query.filter_by(order_id=db_order.id).first()
assert retry_order.order_id == db_order.id
assert retry_order.region_code == region_number_list_to_code(
[Regions.t11n_afr.value, Regions.t11n_eu.value])
post_rv = client.post('/order/tx/1',
data={'regions': [[Regions.t11n_eu.value]]})
assert post_rv.status_code == HTTPStatus.OK
db_order = Order.query.filter_by(uuid=uuid).first()
order_helpers.upsert_retransmission(db_order)
# only t11n_afr confirmations are missing
# expectation is that the existing record in TxRetry gets updated
retry_order = TxRetry.query.filter_by(order_id=db_order.id).all()
assert len(retry_order) == 1
assert retry_order[0].order_id == db_order.id
assert retry_order[0].region_code == region_number_list_to_code(
[Regions.t11n_afr.value])
@patch('orders.new_invoice')
def test_upsert_retransmission_for_order_without_regions(
mock_new_invoice, client):
uuid = generate_test_order(mock_new_invoice,
client,
tx_seq_num=1,
order_status=OrderStatus.transmitting)['uuid']
db_order = Order.query.filter_by(uuid=uuid).first()
post_rv = client.post(
'/order/tx/1',
data={'regions': [[Regions.g18.value, Regions.e113.value]]})
assert post_rv.status_code == HTTPStatus.OK
db_order = Order.query.filter_by(uuid=uuid).first()
order_helpers.upsert_retransmission(db_order)
# No specific region was provided during order creation, so all regions
# should confrim tx
retry_order = TxRetry.query.filter_by(order_id=db_order.id).first()
assert retry_order.order_id == db_order.id
assert retry_order.region_code == region_number_list_to_code([
Regions.t11n_afr.value, Regions.t11n_eu.value, Regions.t18v_c.value,
Regions.t18v_ku.value
])
@patch('orders.new_invoice')
def test_upsert_retransmission_for_non_transmitting_order(
mock_new_invoice, client):
uuid = generate_test_order(mock_new_invoice,
client,
order_status=OrderStatus.paid,
regions=[
Regions.g18.value, Regions.e113.value,
Regions.t11n_afr.value,
Regions.t11n_eu.value
])['uuid']
db_order = Order.query.filter_by(uuid=uuid).first()
order_helpers.upsert_retransmission(db_order)
# There should be no retransmission record for this order yet since it's
# not transmitting
retry_order = TxRetry.query.filter_by(order_id=db_order.id).first()
assert not retry_order
@patch('orders.new_invoice')
def test_get_missing_tx_confirmations(mock_new_invoice, client):
selected_regions = [
Regions.g18.value, Regions.e113.value, 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()
# So far, the confirmations from the selected regions should all be missing
missing_confirmations = order_helpers.get_missing_tx_confirmations(
db_order)
assert len(missing_confirmations) == len(selected_regions)
assert (all([
region_number_to_id(x) in missing_confirmations
for x in selected_regions
]))
# Send some Tx confirmations, including one for a region (T18V Ku) that was
# not part of the original region selection
post_rv = client.post(
'/order/tx/1',
data={
'regions':
[[Regions.g18.value, Regions.e113.value, Regions.t18v_ku.value]]
})
assert post_rv.status_code == HTTPStatus.OK
db_order = Order.query.filter_by(uuid=uuid).first()
missing_confirmations = order_helpers.get_missing_tx_confirmations(
db_order)
# Now, only T11N AFR and T11N EU should be missing
assert (all([
region_number_to_id(x) in missing_confirmations
for x in [Regions.t11n_afr.value, Regions.t11n_eu.value]
]))
@patch('orders.new_invoice')
def test_get_missing_tx_confirmations_for_non_transmitting_order(
mock_new_invoice, client):
uuid = generate_test_order(mock_new_invoice,
client,
tx_seq_num=1,
order_status=OrderStatus.paid,
regions=[
Regions.g18.value, Regions.e113.value,
Regions.t11n_afr.value,
Regions.t11n_eu.value
])['uuid']
db_order = Order.query.filter_by(uuid=uuid).first()
# There should be no missing Tx confirmation record for this order yet
# since it's not transmitting
missing_confirmations = order_helpers.get_missing_tx_confirmations(
db_order)
assert len(missing_confirmations) == 0