From edd9455d7bc38f1c3b751a8d7254996991a4c942 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Thu, 9 Nov 2023 10:33:53 +0000 Subject: [PATCH] Add test publish and view orders --- tests/mocks/cln.py | 4 +- tests/mocks/lnd.py | 26 +++- tests/test_trade_pipeline.py | 227 ++++++++++++++++++++++++++--------- 3 files changed, 189 insertions(+), 68 deletions(-) diff --git a/tests/mocks/cln.py b/tests/mocks/cln.py index 4aaad91c..cc2a8d10 100644 --- a/tests/mocks/cln.py +++ b/tests/mocks/cln.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock class MockNodeStub: - def __init__(channel, other): + def __init__(self, channel): pass def Getinfo(self, request): @@ -34,7 +34,7 @@ class MockNodeStub: class MockHoldStub: - def __init__(channel, other): + def __init__(self, channel): pass def HoldInvoiceLookup(self, request): diff --git a/tests/mocks/lnd.py b/tests/mocks/lnd.py index 53f622b5..ea52b1c5 100644 --- a/tests/mocks/lnd.py +++ b/tests/mocks/lnd.py @@ -4,6 +4,9 @@ from unittest.mock import MagicMock class MockLightningStub: + def __init__(self, channel): + pass + def GetInfo(self, request): response = MagicMock() response.testnet = True @@ -35,6 +38,8 @@ class MockLightningStub: response.payment_addr = '\275\205\224\016\363\325\262\201\306"8\022e\343\215\355\277\304\021\r\037l\202\023\314\353\334\265\002\036h\322' response.num_msat = 1731000 + return response + def CancelInvoice(self, request): response = MagicMock() if ( @@ -76,12 +81,16 @@ class MockLightningStub: class MockInvoicesStub: + def __init__(self, channel): + pass + def AddHoldInvoice(self, request): response = MagicMock() - if request.value == 1731: - response.payment_request = "lntb17310n1pj552mdpp50p2utzh7mpsf3uq7u7cws4a96tj3kyq54hchdkpw8zecamx9klrqd2j2pshjmt9de6zqun9vejhyetwvdjn5gphxs6nsvfe893z6wphvfsj6dryvymj6wp5xvuz6wp5xcukvdec8yukgcf49cs9g6rfwvs8qcted4jkuapq2ay5cnpqgefy2326g5syjn3qt984253q2aq5cnz92skzqcmgv43kkgr0dcs9ymmzdafkzarnyp5kvgr5dpjjqmr0vd4jqampwvs8xatrvdjhxumxw4kzugzfwss8w6tvdssxyefqw4hxcmmrddjkggpgveskjmpfyp6kumr9wdejq7t0w5sxx6r9v96zqmmjyp3kzmnrv4kzqatwd9kxzar9wfskcmre9ccqz52xqzwzsp5hkzegrhn6kegr33z8qfxtcudaklugygdrakgyy7va0wt2qs7drfq9qyyssqc6rztchzl4m7mlulrhlcajszcl9fan8908k9n5x7gmz8g8d6ht5pj4l8r0dushq6j5s8x7yv9a5klz0kfxwy8v6ze6adyrrp4wu0q0sq3t604x" - response.add_index = 1 - response.payment_addr = b'\275\205\224\016\363\325\262\201\306"8\022e\343\215\355\277\304\021\r\037l\202\023\314\353\334\265\002\036h\322' + # if request.value == 1731: + response.payment_request = "lntb17310n1pj552mdpp50p2utzh7mpsf3uq7u7cws4a96tj3kyq54hchdkpw8zecamx9klrqd2j2pshjmt9de6zqun9vejhyetwvdjn5gphxs6nsvfe893z6wphvfsj6dryvymj6wp5xvuz6wp5xcukvdec8yukgcf49cs9g6rfwvs8qcted4jkuapq2ay5cnpqgefy2326g5syjn3qt984253q2aq5cnz92skzqcmgv43kkgr0dcs9ymmzdafkzarnyp5kvgr5dpjjqmr0vd4jqampwvs8xatrvdjhxumxw4kzugzfwss8w6tvdssxyefqw4hxcmmrddjkggpgveskjmpfyp6kumr9wdejq7t0w5sxx6r9v96zqmmjyp3kzmnrv4kzqatwd9kxzar9wfskcmre9ccqz52xqzwzsp5hkzegrhn6kegr33z8qfxtcudaklugygdrakgyy7va0wt2qs7drfq9qyyssqc6rztchzl4m7mlulrhlcajszcl9fan8908k9n5x7gmz8g8d6ht5pj4l8r0dushq6j5s8x7yv9a5klz0kfxwy8v6ze6adyrrp4wu0q0sq3t604x" + response.add_index = 1 + response.payment_addr = b'\275\205\224\016\363\325\262\201\306"8\022e\343\215\355\277\304\021\r\037l\202\023\314\353\334\265\002\036h\322' + return response def CancelInvoice(self, request): response = MagicMock() @@ -93,7 +102,12 @@ class MockInvoicesStub: def LookupInvoiceV2(self, request): response = MagicMock() - return response + if request.payment_hash == bytes.fromhex( + "7855c58afed86098f01ee7b0e857a5d2e51b1014adf176d82e38b38eecc5b7c6" + ): + response.memo = "Payment reference: ..." + response.state = 3 # "ACCEPTED" + return response class MockRouterStub: @@ -117,7 +131,7 @@ class MockSignerStub: class MockVersionerStub: - def __init__(channel, other): + def __init__(self, channel): pass def GetVersion(self, request): diff --git a/tests/test_trade_pipeline.py b/tests/test_trade_pipeline.py index d3b6996e..d6883bed 100644 --- a/tests/test_trade_pipeline.py +++ b/tests/test_trade_pipeline.py @@ -7,15 +7,13 @@ from decouple import config from django.contrib.auth.models import User from django.test import Client, TestCase +from api.management.commands.follow_invoices import Command as FollowInvoices from api.models import Currency, Order from api.tasks import cache_market -from tests.mocks.cln import MockHoldStub, MockNodeStub -from tests.mocks.lnd import ( +from tests.mocks.cln import MockHoldStub # , MockNodeStub +from tests.mocks.lnd import ( # MockRouterStub,; MockSignerStub,; MockVersionerStub, MockInvoicesStub, MockLightningStub, - MockRouterStub, - MockSignerStub, - MockVersionerStub, ) @@ -39,7 +37,7 @@ class TradeTest(TestCase): response = self.client.post(path, data) self.assertEqual(response.status_code, 302) - def get_robot_auth(self, robot_index): + def get_robot_auth(self, robot_index, first_encounter=False): """ Create an AUTH header that embeds token, pub_key, and enc_priv_key into a single string as requested by the robosats token middleware. @@ -51,9 +49,14 @@ class TradeTest(TestCase): with open(f"tests/robots/{robot_index}/enc_priv_key", "r") as file: enc_priv_key = file.read() - headers = { - "HTTP_AUTHORIZATION": f"Token {b91_token} | Public {pub_key} | Private {enc_priv_key}" - } + # First time a robot authenticated, it is registered by the backend, so pub_key and enc_priv_key is needed + if first_encounter: + headers = { + "HTTP_AUTHORIZATION": f"Token {b91_token} | Public {pub_key} | Private {enc_priv_key}" + } + else: + headers = {"HTTP_AUTHORIZATION": f"Token {b91_token}"} + return headers, pub_key, enc_priv_key def assert_robot(self, response, pub_key, enc_priv_key, expected_nickname): @@ -93,7 +96,7 @@ class TradeTest(TestCase): Creates the robots in /tests/robots/{robot_index} """ path = "/api/robot/" - headers, pub_key, enc_priv_key = self.get_robot_auth(robot_index) + headers, pub_key, enc_priv_key = self.get_robot_auth(robot_index, True) response = self.client.get(path, **headers) @@ -125,44 +128,34 @@ class TradeTest(TestCase): usd.timestamp, datetime, "External price timestamp is not a datetime" ) - def test_create_order( - self, - robot_index=1, - payment_method="Advcash Cash F2F", - min_amount=21, - max_amount=101.7, - premium=3.34, - public_duration=69360, - escrow_duration=8700, - bond_size=3.5, - latitude=34.7455, - longitude=135.503, - ): + def create_order(self, maker_form, robot_index=1): # Requisites # Cache market prices self.test_cache_market() path = "/api/make/" # Get valid robot auth headers - headers, _, _ = self.get_robot_auth(robot_index) + headers, _, _ = self.get_robot_auth(robot_index, True) - # Prepare request body + response = self.client.post(path, maker_form, **headers) + return response + + def test_create_order(self): maker_form = { "type": Order.Types.BUY, "currency": 1, "has_range": True, - "min_amount": min_amount, - "max_amount": max_amount, - "payment_method": payment_method, + "min_amount": 21, + "max_amount": 101.7, + "payment_method": "Advcash Cash F2F", "is_explicit": False, - "premium": premium, - "public_duration": public_duration, - "escrow_duration": escrow_duration, - "bond_size": bond_size, - "latitude": latitude, - "longitude": longitude, + "premium": 3.34, + "public_duration": 69360, + "escrow_duration": 8700, + "bond_size": 3.5, + "latitude": 34.7455, + "longitude": 135.503, } - - response = self.client.post(path, maker_form, **headers) + response = self.create_order(maker_form, robot_index=1) data = json.loads(response.content.decode()) # Checks @@ -191,32 +184,44 @@ class TradeTest(TestCase): ) self.assertTrue(data["has_range"], "Order with range has a False has_range") self.assertAlmostEqual( - float(data["min_amount"]), min_amount, "Order min amount does not match" + float(data["min_amount"]), + maker_form["min_amount"], + "Order min amount does not match", ) self.assertAlmostEqual( - float(data["max_amount"]), max_amount, "Order max amount does not match" + float(data["max_amount"]), + maker_form["max_amount"], + "Order max amount does not match", ) self.assertEqual( data["payment_method"], - payment_method, + maker_form["payment_method"], "Order payment method does not match", ) self.assertEqual( data["escrow_duration"], - escrow_duration, + maker_form["escrow_duration"], "Order escrow duration does not match", ) self.assertAlmostEqual( - float(data["bond_size"]), bond_size, "Order bond size does not match" + float(data["bond_size"]), + maker_form["bond_size"], + "Order bond size does not match", ) self.assertAlmostEqual( - float(data["latitude"]), latitude, "Order latitude does not match" + float(data["latitude"]), + maker_form["latitude"], + "Order latitude does not match", ) self.assertAlmostEqual( - float(data["longitude"]), longitude, "Order longitude does not match" + float(data["longitude"]), + maker_form["longitude"], + "Order longitude does not match", ) self.assertAlmostEqual( - float(data["premium"]), premium, "Order premium does not match" + float(data["premium"]), + maker_form["premium"], + "Order premium does not match", ) self.assertFalse( data["is_explicit"], "Relative pricing order has True is_explicit" @@ -226,23 +231,125 @@ class TradeTest(TestCase): ) self.assertIsNone(data["taker"], "New order's taker is not null") - @patch("api.lightning.cln.node_pb2_grpc.NodeStub", MockNodeStub) + return data + @patch("api.lightning.cln.hold_pb2_grpc.HoldStub", MockHoldStub) - @patch("api.lightning.lnd.verrpc_pb2_grpc.VersionerStub", MockVersionerStub) @patch("api.lightning.lnd.lightning_pb2_grpc.LightningStub", MockLightningStub) @patch("api.lightning.lnd.invoices_pb2_grpc.InvoicesStub", MockInvoicesStub) - @patch("api.lightning.lnd.router_pb2_grpc.RouterStub", MockRouterStub) - @patch("api.lightning.lnd.signer_pb2_grpc.SignerStub", MockSignerStub) - def test_maker_bond_locked(self): - self.test_create_order( - robot_index=1, - payment_method="Cash F2F", - min_amount=80, - max_amount=500, - premium=5, - public_duration=86000, - escrow_duration=8000, - bond_size=2, - latitude=0, - longitude=0, + def get_order(self, order_id, robot_index=1, first_encounter=False): + path = "/api/order/" + params = f"?order_id={order_id}" + headers, _, _ = self.get_robot_auth(robot_index, first_encounter) + response = self.client.get(path + params, **headers) + + return response + + def test_get_order_created(self): + # Make an order + maker_form = { + "type": Order.Types.BUY, + "currency": 1, + "has_range": True, + "min_amount": 21, + "max_amount": 101.7, + "payment_method": "Advcash Cash F2F", + "is_explicit": False, + "premium": 3.34, + "public_duration": 69360, + "escrow_duration": 8700, + "bond_size": 3.5, + "latitude": 34.7455, + "longitude": 135.503, + } + order_made_response = self.create_order(maker_form, robot_index=1) + order_made_data = json.loads(order_made_response.content.decode()) + + # Maker's first order fetch. Should trigger maker bond hold invoice generation. + response = self.get_order(order_made_data["id"]) + data = json.loads(response.content.decode()) + + self.assertEqual(response.status_code, 200) + self.assertEqual(data["id"], order_made_data["id"]) + self.assertTrue( + isinstance(datetime.fromisoformat(data["created_at"]), datetime) ) + self.assertTrue( + isinstance(datetime.fromisoformat(data["expires_at"]), datetime) + ) + self.assertTrue(data["is_maker"]) + self.assertTrue(data["is_participant"]) + self.assertTrue(data["is_buyer"]) + self.assertFalse(data["is_seller"]) + self.assertEqual(data["maker_status"], "Active") + self.assertEqual(data["status_message"], Order.Status(Order.Status.WFB).label) + self.assertFalse(data["is_fiat_sent"]) + self.assertFalse(data["is_disputed"]) + self.assertEqual(data["ur_nick"], "MyopicRacket333") + self.assertTrue(isinstance(data["satoshis_now"], int)) + self.assertFalse(data["maker_locked"]) + self.assertFalse(data["taker_locked"]) + self.assertFalse(data["escrow_locked"]) + self.assertEqual( + data["bond_invoice"], + "lntb17310n1pj552mdpp50p2utzh7mpsf3uq7u7cws4a96tj3kyq54hchdkpw8zecamx9klrqd2j2pshjmt9de6zqun9vejhyetwvdjn5gphxs6nsvfe893z6wphvfsj6dryvymj6wp5xvuz6wp5xcukvdec8yukgcf49cs9g6rfwvs8qcted4jkuapq2ay5cnpqgefy2326g5syjn3qt984253q2aq5cnz92skzqcmgv43kkgr0dcs9ymmzdafkzarnyp5kvgr5dpjjqmr0vd4jqampwvs8xatrvdjhxumxw4kzugzfwss8w6tvdssxyefqw4hxcmmrddjkggpgveskjmpfyp6kumr9wdejq7t0w5sxx6r9v96zqmmjyp3kzmnrv4kzqatwd9kxzar9wfskcmre9ccqz52xqzwzsp5hkzegrhn6kegr33z8qfxtcudaklugygdrakgyy7va0wt2qs7drfq9qyyssqc6rztchzl4m7mlulrhlcajszcl9fan8908k9n5x7gmz8g8d6ht5pj4l8r0dushq6j5s8x7yv9a5klz0kfxwy8v6ze6adyrrp4wu0q0sq3t604x", + ) + self.assertTrue(isinstance(data["bond_satoshis"], int)) + + @patch("api.lightning.lnd.invoices_pb2_grpc.InvoicesStub", MockInvoicesStub) + def check_for_locked_bonds(self): + # A background thread checks every 5 second the status of invoices. We invoke directly during test. + # It will ask LND via gRPC. In our test, the request/response from LND is mocked, and it will return fake invoice status "ACCEPTED" + follow_invoices = FollowInvoices() + follow_invoices.follow_hold_invoices() + + def create_and_publish_order(self, maker_form, robot_index=1): + # Make an order + order_made_response = self.create_order(maker_form, robot_index=1) + order_made_data = json.loads(order_made_response.content.decode()) + + # Maker's first order fetch. Should trigger maker bond hold invoice generation. + self.get_order(order_made_data["id"]) + + # Check for invoice locked (the mocked LND will return ACCEPTED) + self.check_for_locked_bonds() + + # Get order + response = self.get_order(order_made_data["id"]) + return response + + def test_publish_order(self): + maker_form = { + "type": Order.Types.BUY, + "currency": 1, + "has_range": True, + "min_amount": 21, + "max_amount": 101.7, + "payment_method": "Advcash Cash F2F", + "is_explicit": False, + "premium": 3.34, + "public_duration": 69360, + "escrow_duration": 8700, + "bond_size": 3.5, + "latitude": 34.7455, + "longitude": 135.503, + } + # Get order + response = self.create_and_publish_order(maker_form) + data = json.loads(response.content.decode()) + + self.assertEqual(response.status_code, 200) + self.assertEqual(data["id"], data["id"]) + self.assertEqual(data["status_message"], Order.Status(Order.Status.PUB).label) + self.assertTrue(data["maker_locked"]) + self.assertFalse(data["taker_locked"]) + self.assertFalse(data["escrow_locked"]) + + # Test what we can see with newly created robot 2 (only for public status) + public_response = self.get_order( + data["id"], robot_index=2, first_encounter=True + ) + public_data = json.loads(public_response.content.decode()) + + self.assertFalse(public_data["is_participant"]) + self.assertTrue(isinstance(public_data["price_now"], float)) + self.assertTrue(isinstance(data["satoshis_now"], int))