From 62ef86f1b419d63bd64bc36a3116bde6ff5906d1 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Wed, 22 Nov 2023 13:16:25 +0000 Subject: [PATCH] Add order expiry tests --- api/management/commands/clean_orders.py | 93 ++++++++-------- tests/test_trade_pipeline.py | 140 +++++++++++++++++++++++- tests/utils/__init__.py | 0 tests/{node_utils.py => utils/node.py} | 0 tests/{pgp_utils.py => utils/pgp.py} | 0 5 files changed, 180 insertions(+), 53 deletions(-) create mode 100644 tests/utils/__init__.py rename tests/{node_utils.py => utils/node.py} (100%) rename tests/{pgp_utils.py => utils/pgp.py} (100%) diff --git a/api/management/commands/clean_orders.py b/api/management/commands/clean_orders.py index 989b9087..ded40335 100644 --- a/api/management/commands/clean_orders.py +++ b/api/management/commands/clean_orders.py @@ -8,68 +8,65 @@ from api.models import Order class Command(BaseCommand): - help = "Follows all active hold invoices" + help = "Follows all active orders and make them expire if needed." - # def add_arguments(self, parser): - # parser.add_argument('debug', nargs='+', type=boolean) + do_nothing = [ + Order.Status.UCA, + Order.Status.EXP, + Order.Status.DIS, + Order.Status.CCA, + Order.Status.PAY, + Order.Status.SUC, + Order.Status.FAI, + Order.Status.MLD, + Order.Status.TLD, + Order.Status.WFR, + ] - def clean_orders(self, *args, **options): - """Continuously checks order expiration times for 1 hour. If order + def clean_orders(self): + """Continuously checks order expiration times. If order has expires, it calls the logics module for expiration handling.""" - do_nothing = [ - Order.Status.UCA, - Order.Status.EXP, - Order.Status.DIS, - Order.Status.CCA, - Order.Status.PAY, - Order.Status.SUC, - Order.Status.FAI, - Order.Status.MLD, - Order.Status.TLD, - Order.Status.WFR, - ] + queryset = Order.objects.exclude(status__in=self.do_nothing) + queryset = queryset.filter( + expires_at__lt=timezone.now() + ) # expires at lower than now - while True: - time.sleep(5) + debug = {} + debug["num_expired_orders"] = len(queryset) + debug["expired_orders"] = [] + debug["failed_order_expiry"] = [] + debug["reason_failure"] = [] - queryset = Order.objects.exclude(status__in=do_nothing) - queryset = queryset.filter( - expires_at__lt=timezone.now() - ) # expires at lower than now + for idx, order in enumerate(queryset): + context = str(order) + " was " + Order.Status(order.status).label + try: + if Logics.order_expires(order): # Order send to expire here + debug["expired_orders"].append({idx: context}) - debug = {} - debug["num_expired_orders"] = len(queryset) - debug["expired_orders"] = [] - debug["failed_order_expiry"] = [] - debug["reason_failure"] = [] + # It should not happen, but if it cannot locate the hold invoice + # it probably was cancelled by another thread, make it expire anyway. + except Exception as e: + debug["failed_order_expiry"].append({idx: context}) + debug["reason_failure"].append({idx: str(e)}) - for idx, order in enumerate(queryset): - context = str(order) + " was " + Order.Status(order.status).label - try: - if Logics.order_expires(order): # Order send to expire here - debug["expired_orders"].append({idx: context}) + if "unable to locate invoice" in str(e): + self.stdout.write(str(e)) + order.update_status(Order.Status.EXP) + debug["expired_orders"].append({idx: context}) - # It should not happen, but if it cannot locate the hold invoice - # it probably was cancelled by another thread, make it expire anyway. - except Exception as e: - debug["failed_order_expiry"].append({idx: context}) - debug["reason_failure"].append({idx: str(e)}) - - if "unable to locate invoice" in str(e): - self.stdout.write(str(e)) - order.update_status(Order.Status.EXP) - debug["expired_orders"].append({idx: context}) - - if debug["num_expired_orders"] > 0: - self.stdout.write(str(timezone.now())) - self.stdout.write(str(debug)) + if debug["num_expired_orders"] > 0: + self.stdout.write(str(timezone.now())) + self.stdout.write(str(debug)) def handle(self, *args, **options): """Never mind database locked error, keep going, print them out. Not an issue with PostgresQL""" try: - self.clean_orders() + while True: + self.clean_orders() + time.sleep(5) + except Exception as e: if "database is locked" in str(e): self.stdout.write("database is locked") diff --git a/tests/test_trade_pipeline.py b/tests/test_trade_pipeline.py index 92a3caf5..12aed546 100644 --- a/tests/test_trade_pipeline.py +++ b/tests/test_trade_pipeline.py @@ -7,19 +7,20 @@ from decouple import config from django.contrib.auth.models import User from django.urls import reverse +from api.management.commands.clean_orders import Command as CleanOrders from api.management.commands.follow_invoices import Command as FollowInvoices from api.models import Currency, Order from api.tasks import cache_market, follow_send_payment from control.models import BalanceLog from control.tasks import compute_node_balance, do_accounting -from tests.node_utils import ( +from tests.test_api import BaseAPITestCase +from tests.utils.node import ( add_invoice, create_address, pay_invoice, set_up_regtest_network, ) -from tests.pgp_utils import sign_message -from tests.test_api import BaseAPITestCase +from tests.utils.pgp import sign_message def read_file(file_path): @@ -358,8 +359,13 @@ class TradeTest(BaseAPITestCase): def follow_hold_invoices(self): # A background thread checks every 5 second the status of invoices. We invoke directly during test. - follow_invoices = FollowInvoices() - follow_invoices.follow_hold_invoices() + follower = FollowInvoices() + follower.follow_hold_invoices() + + def clean_orders(self): + # A background thread checks every 5 second order expirations. We invoke directly during test. + cleaner = CleanOrders() + cleaner.clean_orders() def send_payments(self): # A background thread checks every 5 second whether there are outgoing payments. We invoke directly during test. @@ -906,6 +912,130 @@ class TradeTest(BaseAPITestCase): data["bad_request"], "This order has been cancelled collaborativelly" ) + def test_created_order_expires(self): + """ + Tests the expiration of a public order + """ + maker_form = self.maker_form_buy_with_range + response = self.make_order(maker_form) + + # Change order expiry to now + order = Order.objects.get(id=response.json()["id"]) + order.expires_at = datetime.now() + order.save() + + # Make orders expire + self.clean_orders() + + response = self.get_order(response.json()["id"]) + data = response.json() + + self.assertEqual(response.status_code, 200) + self.assertResponse(response) + + self.assertEqual( + data["status"], + Order.Status.EXP, + ) + self.assertEqual( + data["expiry_message"], + Order.ExpiryReasons(Order.ExpiryReasons.NMBOND).label, + ) + self.assertEqual(data["expiry_reason"], Order.ExpiryReasons.NMBOND) + + def test_public_order_expires(self): + """ + Tests the expiration of a public order + """ + maker_form = self.maker_form_buy_with_range + response = self.make_and_publish_order(maker_form) + + # Change order expiry to now + order = Order.objects.get(id=response.json()["id"]) + order.expires_at = datetime.now() + order.save() + + # Make orders expire + self.clean_orders() + + response = self.get_order(response.json()["id"]) + data = response.json() + + self.assertEqual(response.status_code, 200) + self.assertResponse(response) + + self.assertEqual( + data["status"], + Order.Status.EXP, + ) + self.assertEqual( + data["expiry_message"], + Order.ExpiryReasons(Order.ExpiryReasons.NTAKEN).label, + ) + self.assertEqual(data["expiry_reason"], Order.ExpiryReasons.NTAKEN) + + def test_taken_order_expires(self): + """ + Tests the expiration of a public order + """ + maker_form = self.maker_form_buy_with_range + response = self.make_and_lock_contract(maker_form) + + # Change order expiry to now + order = Order.objects.get(id=response.json()["id"]) + order.expires_at = datetime.now() + order.save() + + # Make orders expire + self.clean_orders() + + response = self.get_order(response.json()["id"]) + data = response.json() + + self.assertEqual(response.status_code, 200) + self.assertResponse(response) + + self.assertEqual( + data["status"], + Order.Status.EXP, + ) + self.assertEqual( + data["expiry_message"], + Order.ExpiryReasons(Order.ExpiryReasons.NESINV).label, + ) + self.assertEqual(data["expiry_reason"], Order.ExpiryReasons.NESINV) + + def test_escrow_locked_expires(self): + """ + Tests the expiration of a public order + """ + maker_form = self.maker_form_buy_with_range + response = self.trade_to_locked_escrow(maker_form) + + # Change order expiry to now + order = Order.objects.get(id=response.json()["id"]) + order.expires_at = datetime.now() + order.save() + + # Make orders expire + self.clean_orders() + + response = self.get_order(response.json()["id"]) + data = response.json() + + self.assertEqual(response.status_code, 200) + self.assertResponse(response) + + self.assertEqual( + data["status"], + Order.Status.EXP, + ) + self.assertEqual( + data["expiry_message"], + Order.ExpiryReasons(Order.ExpiryReasons.NINVOI).label, + ) + self.assertEqual(data["expiry_reason"], Order.ExpiryReasons.NINVOI) + def test_ticks(self): """ Tests the historical ticks serving endpoint after creating a contract diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/node_utils.py b/tests/utils/node.py similarity index 100% rename from tests/node_utils.py rename to tests/utils/node.py diff --git a/tests/pgp_utils.py b/tests/utils/pgp.py similarity index 100% rename from tests/pgp_utils.py rename to tests/utils/pgp.py