Add testing against API specs with assertResponse

This commit is contained in:
Reckless_Satoshi 2023-11-11 15:48:54 +00:00 committed by Reckless_Satoshi
parent edd9455d7b
commit b4fe30e733
12 changed files with 2324 additions and 151 deletions

View File

@ -30,7 +30,7 @@ POSTGRES_HOST='127.0.0.1'
POSTGRES_PORT='5432' POSTGRES_PORT='5432'
# Tor proxy for remote calls (e.g. fetching prices or sending Telegram messages) # Tor proxy for remote calls (e.g. fetching prices or sending Telegram messages)
USE_TOR='True' USE_TOR=True
TOR_PROXY='127.0.0.1:9050' TOR_PROXY='127.0.0.1:9050'
# Auto unlock LND password. Only used in development docker-compose environment. # Auto unlock LND password. Only used in development docker-compose environment.
@ -166,4 +166,4 @@ MINIMUM_TARGET_CONF = 24
SLASHED_BOND_REWARD_SPLIT = 0.5 SLASHED_BOND_REWARD_SPLIT = 0.5
# Username for HTLCs escrows # Username for HTLCs escrows
ESCROW_USERNAME = 'admin' ESCROW_USERNAME = 'admin'

View File

@ -1,3 +1,4 @@
# We use custom seeded UUID generation during testing
import uuid import uuid
from decouple import config from decouple import config
@ -9,6 +10,19 @@ from django.db.models.signals import pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
if config("COORDINATOR_TESTING", cast=bool, default=False):
import random
import string
random.seed(1)
chars = string.ascii_lowercase + string.digits
def custom_uuid():
return uuid.uuid5(uuid.NAMESPACE_DNS, "".join(random.choices(chars, k=20)))
else:
custom_uuid = uuid.uuid4
class Order(models.Model): class Order(models.Model):
class Types(models.IntegerChoices): class Types(models.IntegerChoices):
@ -44,7 +58,7 @@ class Order(models.Model):
NESINV = 4, "Neither escrow locked or invoice submitted" NESINV = 4, "Neither escrow locked or invoice submitted"
# order info # order info
reference = models.UUIDField(default=uuid.uuid4, editable=False) reference = models.UUIDField(default=custom_uuid, editable=False)
status = models.PositiveSmallIntegerField( status = models.PositiveSmallIntegerField(
choices=Status.choices, null=False, default=Status.WFB choices=Status.choices, null=False, default=Status.WFB
) )

View File

@ -5,6 +5,7 @@ from django.conf import settings
from drf_spectacular.utils import OpenApiExample, OpenApiParameter from drf_spectacular.utils import OpenApiExample, OpenApiParameter
from api.serializers import ( from api.serializers import (
InfoSerializer,
ListOrderSerializer, ListOrderSerializer,
OrderDetailSerializer, OrderDetailSerializer,
StealthSerializer, StealthSerializer,
@ -322,17 +323,7 @@ class OrderViewSchema:
), ),
], ],
"responses": { "responses": {
200: { 200: OrderDetailSerializer,
"type": "object",
"additionalProperties": {
"oneOf": [
{"type": "str"},
{"type": "number"},
{"type": "object"},
{"type": "boolean"},
],
},
},
400: { 400: {
"type": "object", "type": "object",
"properties": { "properties": {
@ -474,6 +465,16 @@ class RobotViewSchema:
"type": "integer", "type": "integer",
"description": "Last order id if present", "description": "Last order id if present",
}, },
"earned_rewards": {
"type": "integer",
"description": "Satoshis available to be claimed",
},
"last_login": {
"type": "string",
"format": "date-time",
"nullable": True,
"description": "Last time the coordinator saw this robot",
},
}, },
}, },
}, },
@ -517,6 +518,9 @@ class InfoViewSchema:
- on-chain swap fees - on-chain swap fees
""" """
), ),
"responses": {
200: InfoSerializer,
},
} }

View File

@ -6,13 +6,19 @@ from .models import MarketTick, Order
RETRY_TIME = int(config("RETRY_TIME")) RETRY_TIME = int(config("RETRY_TIME"))
class VersionSerializer(serializers.Serializer):
major = serializers.IntegerField()
minor = serializers.IntegerField()
patch = serializers.IntegerField()
class InfoSerializer(serializers.Serializer): class InfoSerializer(serializers.Serializer):
num_public_buy_orders = serializers.IntegerField() num_public_buy_orders = serializers.IntegerField()
num_public_sell_orders = serializers.IntegerField() num_public_sell_orders = serializers.IntegerField()
book_liquidity = serializers.IntegerField( book_liquidity = serializers.IntegerField(
help_text="Total amount of BTC in the order book" help_text="Total amount of BTC in the order book"
) )
active_robots_today = serializers.CharField() active_robots_today = serializers.IntegerField()
last_day_nonkyc_btc_premium = serializers.FloatField( last_day_nonkyc_btc_premium = serializers.FloatField(
help_text="Average premium (weighted by volume) of the orders in the last 24h" help_text="Average premium (weighted by volume) of the orders in the last 24h"
) )
@ -23,6 +29,7 @@ class InfoSerializer(serializers.Serializer):
help_text="Total volume in BTC since exchange's inception" help_text="Total volume in BTC since exchange's inception"
) )
lnd_version = serializers.CharField() lnd_version = serializers.CharField()
cln_version = serializers.CharField()
robosats_running_commit_hash = serializers.CharField() robosats_running_commit_hash = serializers.CharField()
alternative_site = serializers.CharField() alternative_site = serializers.CharField()
alternative_name = serializers.CharField() alternative_name = serializers.CharField()
@ -35,6 +42,17 @@ class InfoSerializer(serializers.Serializer):
current_swap_fee_rate = serializers.FloatField( current_swap_fee_rate = serializers.FloatField(
help_text="Swap fees to perform on-chain transaction (percent)" help_text="Swap fees to perform on-chain transaction (percent)"
) )
version = VersionSerializer()
notice_severity = serializers.ChoiceField(
choices=[
("none", "none"),
("warning", "warning"),
("success", "success"),
("error", "error"),
("info", "info"),
]
)
notice_message = serializers.CharField()
class ListOrderSerializer(serializers.ModelSerializer): class ListOrderSerializer(serializers.ModelSerializer):
@ -60,7 +78,7 @@ class ListOrderSerializer(serializers.ModelSerializer):
"escrow_duration", "escrow_duration",
"bond_size", "bond_size",
"latitude", "latitude",
"longitude" "longitude",
) )
@ -160,10 +178,13 @@ class OrderDetailSerializer(serializers.ModelSerializer):
required=False, required=False,
help_text="Price of the order in the order's currency at the time of request (upto 5 significant digits)", help_text="Price of the order in the order's currency at the time of request (upto 5 significant digits)",
) )
premium = serializers.IntegerField( premium = serializers.CharField(
required=False, help_text="Premium over the CEX price set by the maker"
)
premium_now = serializers.FloatField(
required=False, help_text="Premium over the CEX price at the current time" required=False, help_text="Premium over the CEX price at the current time"
) )
premium_percentile = serializers.IntegerField( premium_percentile = serializers.FloatField(
required=False, required=False,
help_text="(Only if `is_maker`) Premium percentile of your order compared to other public orders in the same currency currently in the order book", help_text="(Only if `is_maker`) Premium percentile of your order compared to other public orders in the same currency currently in the order book",
) )
@ -253,11 +274,11 @@ class OrderDetailSerializer(serializers.ModelSerializer):
required=False, required=False,
help_text="in percentage, the swap fee rate the platform charges", help_text="in percentage, the swap fee rate the platform charges",
) )
latitude = serializers.FloatField( latitude = serializers.CharField(
required=False, required=False,
help_text="Latitude of the order for F2F payments", help_text="Latitude of the order for F2F payments",
) )
longitude = serializers.FloatField( longitude = serializers.CharField(
required=False, required=False,
help_text="Longitude of the order for F2F payments", help_text="Longitude of the order for F2F payments",
) )
@ -300,7 +321,11 @@ class OrderDetailSerializer(serializers.ModelSerializer):
) )
maker_summary = SummarySerializer(required=False) maker_summary = SummarySerializer(required=False)
taker_summary = SummarySerializer(required=False) taker_summary = SummarySerializer(required=False)
platform_summary = PlatformSummarySerializer(required=True) satoshis_now = serializers.IntegerField(
required=False,
help_text="Maximum size of the order right now in Satoshis",
)
platform_summary = PlatformSummarySerializer(required=False)
expiry_message = serializers.CharField( expiry_message = serializers.CharField(
required=False, required=False,
help_text="The reason the order expired (message associated with the `expiry_reason`)", help_text="The reason the order expired (message associated with the `expiry_reason`)",
@ -338,7 +363,9 @@ class OrderDetailSerializer(serializers.ModelSerializer):
"payment_method", "payment_method",
"is_explicit", "is_explicit",
"premium", "premium",
"premium_now",
"satoshis", "satoshis",
"satoshis_now",
"maker", "maker",
"taker", "taker",
"escrow_duration", "escrow_duration",
@ -350,7 +377,6 @@ class OrderDetailSerializer(serializers.ModelSerializer):
"maker_status", "maker_status",
"taker_status", "taker_status",
"price_now", "price_now",
"premium",
"premium_percentile", "premium_percentile",
"num_similar_orders", "num_similar_orders",
"tg_enabled", "tg_enabled",
@ -441,7 +467,7 @@ class OrderPublicSerializer(serializers.ModelSerializer):
"satoshis_now", "satoshis_now",
"bond_size", "bond_size",
"latitude", "latitude",
"longitude" "longitude",
) )
@ -482,7 +508,7 @@ class MakeOrderSerializer(serializers.ModelSerializer):
"escrow_duration", "escrow_duration",
"bond_size", "bond_size",
"latitude", "latitude",
"longitude" "longitude",
) )

View File

@ -20,19 +20,20 @@ from .views import (
urlpatterns = [ urlpatterns = [
path("schema/", SpectacularAPIView.as_view(), name="schema"), path("schema/", SpectacularAPIView.as_view(), name="schema"),
path("", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), path("", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
path("make/", MakerView.as_view()), path("make/", MakerView.as_view(), name="make"),
path( path(
"order/", "order/",
OrderView.as_view({"get": "get", "post": "take_update_confirm_dispute_cancel"}), OrderView.as_view({"get": "get", "post": "take_update_confirm_dispute_cancel"}),
name="order",
), ),
path("robot/", RobotView.as_view()), path("robot/", RobotView.as_view(), name="robot"),
path("book/", BookView.as_view()), path("book/", BookView.as_view(), name="book"),
path("info/", InfoView.as_view()), path("info/", InfoView.as_view({"get": "get"}), name="info"),
path("price/", PriceView.as_view()), path("price/", PriceView.as_view(), name="price"),
path("limits/", LimitView.as_view()), path("limits/", LimitView.as_view(), name="limits"),
path("reward/", RewardView.as_view()), path("reward/", RewardView.as_view(), name="reward"),
path("historical/", HistoricalView.as_view()), path("historical/", HistoricalView.as_view(), name="historical"),
path("ticks/", TickView.as_view()), path("ticks/", TickView.as_view(), name="ticks"),
path("stealth/", StealthView.as_view()), path("stealth/", StealthView.as_view(), name="stealth"),
path("chat/", ChatView.as_view({"get": "get", "post": "post"})), path("chat/", ChatView.as_view({"get": "get", "post": "post"}), name="chat"),
] ]

View File

@ -724,7 +724,7 @@ class BookView(ListAPIView):
return Response(book_data, status=status.HTTP_200_OK) return Response(book_data, status=status.HTTP_200_OK)
class InfoView(ListAPIView): class InfoView(viewsets.ViewSet):
serializer_class = InfoSerializer serializer_class = InfoSerializer
@extend_schema(**InfoViewSchema.get) @extend_schema(**InfoViewSchema.get)

View File

@ -140,7 +140,7 @@ paths:
description: '' description: ''
/api/info/: /api/info/:
get: get:
operationId: info_list operationId: info_retrieve
description: |2 description: |2
Get general info (overview) about the exchange. Get general info (overview) about the exchange.
@ -172,9 +172,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
type: array $ref: '#/components/schemas/Info'
items:
$ref: '#/components/schemas/Info'
description: '' description: ''
/api/limits/: /api/limits/:
get: get:
@ -563,13 +561,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
type: object $ref: '#/components/schemas/OrderDetail'
additionalProperties:
oneOf:
- type: str
- type: number
- type: object
- type: boolean
description: '' description: ''
'400': '400':
content: content:
@ -1103,7 +1095,7 @@ components:
type: integer type: integer
description: Total amount of BTC in the order book description: Total amount of BTC in the order book
active_robots_today: active_robots_today:
type: string type: integer
last_day_nonkyc_btc_premium: last_day_nonkyc_btc_premium:
type: number type: number
format: double format: double
@ -1119,6 +1111,8 @@ components:
description: Total volume in BTC since exchange's inception description: Total volume in BTC since exchange's inception
lnd_version: lnd_version:
type: string type: string
cln_version:
type: string
robosats_running_commit_hash: robosats_running_commit_hash:
type: string type: string
alternative_site: alternative_site:
@ -1147,12 +1141,19 @@ components:
type: number type: number
format: double format: double
description: Swap fees to perform on-chain transaction (percent) description: Swap fees to perform on-chain transaction (percent)
version:
$ref: '#/components/schemas/Version'
notice_severity:
$ref: '#/components/schemas/NoticeSeverityEnum'
notice_message:
type: string
required: required:
- active_robots_today - active_robots_today
- alternative_name - alternative_name
- alternative_site - alternative_site
- bond_size - bond_size
- book_liquidity - book_liquidity
- cln_version
- current_swap_fee_rate - current_swap_fee_rate
- last_day_nonkyc_btc_premium - last_day_nonkyc_btc_premium
- last_day_volume - last_day_volume
@ -1162,10 +1163,13 @@ components:
- network - network
- node_alias - node_alias
- node_id - node_id
- notice_message
- notice_severity
- num_public_buy_orders - num_public_buy_orders
- num_public_sell_orders - num_public_sell_orders
- robosats_running_commit_hash - robosats_running_commit_hash
- taker_fee - taker_fee
- version
ListOrder: ListOrder:
type: object type: object
properties: properties:
@ -1355,6 +1359,20 @@ components:
required: required:
- currency - currency
- id - id
NoticeSeverityEnum:
enum:
- none
- warning
- success
- error
- info
type: string
description: |-
* `none` - none
* `warning` - warning
* `success` - success
* `error` - error
* `info` - info
NullEnum: NullEnum:
enum: enum:
- null - null
@ -1952,6 +1970,19 @@ components:
nullable: true nullable: true
required: required:
- action - action
Version:
type: object
properties:
major:
type: integer
minor:
type: integer
patch:
type: integer
required:
- major
- minor
- patch
securitySchemes: securitySchemes:
tokenAuth: tokenAuth:
type: apiKey type: apiKey

View File

@ -275,9 +275,9 @@ MAX_PUBLIC_ORDER_DURATION = 24
MIN_PUBLIC_ORDER_DURATION = 0.166 MIN_PUBLIC_ORDER_DURATION = 0.166
# Bond size as percentage (%) # Bond size as percentage (%)
DEFAULT_BOND_SIZE = 3 DEFAULT_BOND_SIZE = float(3)
MIN_BOND_SIZE = 2 MIN_BOND_SIZE = float(2)
MAX_BOND_SIZE = 15 MAX_BOND_SIZE = float(15)
# Default time to provide a valid invoice and the trade escrow MINUTES # Default time to provide a valid invoice and the trade escrow MINUTES
INVOICE_AND_ESCROW_DURATION = 180 INVOICE_AND_ESCROW_DURATION = 180

2004
tests/api_specs.yaml Normal file

File diff suppressed because it is too large Load Diff

28
tests/test_api.py Normal file
View File

@ -0,0 +1,28 @@
import urllib.request
from openapi_tester.schema_tester import SchemaTester
from rest_framework.response import Response
from rest_framework.test import APITestCase
# Update api specs to the newest from a running django server (if any)
try:
urllib.request.urlretrieve(
"http://127.0.0.1:8000/api/schema", "tests/api_specs.yaml"
)
except Exception as e:
print(f"Could not fetch current API specs: {e}")
print("Using previously existing api_specs.yaml definitions")
schema_tester = SchemaTester(schema_file_path="tests/api_specs.yaml")
class BaseAPITestCase(APITestCase):
@staticmethod
def assertResponse(response: Response, **kwargs) -> None:
"""helper to run validate_response and pass kwargs to it"""
# List of endpoints with no available OpenAPI schema
skip_paths = ["/coordinator/login/"]
if response.request["PATH_INFO"] not in skip_paths:
schema_tester.validate_response(response=response, **kwargs)

View File

@ -4,10 +4,12 @@ from unittest.mock import patch
from decouple import config from decouple import config
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import Client, TestCase from django.test import Client
from django.urls import reverse
from tests.mocks.cln import MockNodeStub from tests.mocks.cln import MockNodeStub
from tests.mocks.lnd import MockVersionerStub from tests.mocks.lnd import MockVersionerStub
from tests.test_api import BaseAPITestCase
FEE = config("FEE", cast=float, default=0.2) FEE = config("FEE", cast=float, default=0.2)
NODE_ID = config("NODE_ID", cast=str, default="033b58d7......") NODE_ID = config("NODE_ID", cast=str, default="033b58d7......")
@ -18,7 +20,7 @@ NOTICE_SEVERITY = config("NOTICE_SEVERITY", cast=str, default="none")
NOTICE_MESSAGE = config("NOTICE_MESSAGE", cast=str, default="") NOTICE_MESSAGE = config("NOTICE_MESSAGE", cast=str, default="")
class CoordinatorInfoTest(TestCase): class CoordinatorInfoTest(BaseAPITestCase):
su_pass = "12345678" su_pass = "12345678"
su_name = config("ESCROW_USERNAME", cast=str, default="admin") su_name = config("ESCROW_USERNAME", cast=str, default="admin")
@ -32,12 +34,14 @@ class CoordinatorInfoTest(TestCase):
@patch("api.lightning.cln.node_pb2_grpc.NodeStub", MockNodeStub) @patch("api.lightning.cln.node_pb2_grpc.NodeStub", MockNodeStub)
@patch("api.lightning.lnd.verrpc_pb2_grpc.VersionerStub", MockVersionerStub) @patch("api.lightning.lnd.verrpc_pb2_grpc.VersionerStub", MockVersionerStub)
def test_info(self): def test_info(self):
path = "/api/info/" path = reverse("info")
response = self.client.get(path) response = self.client.get(path)
data = json.loads(response.content.decode()) data = json.loads(response.content.decode())
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertResponse(response)
self.assertEqual(data["num_public_buy_orders"], 0) self.assertEqual(data["num_public_buy_orders"], 0)
self.assertEqual(data["num_public_sell_orders"], 0) self.assertEqual(data["num_public_sell_orders"], 0)
self.assertEqual(data["book_liquidity"], 0) self.assertEqual(data["book_liquidity"], 0)

View File

@ -5,7 +5,7 @@ from unittest.mock import patch
from decouple import config from decouple import config
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import Client, TestCase from django.urls import reverse
from api.management.commands.follow_invoices import Command as FollowInvoices from api.management.commands.follow_invoices import Command as FollowInvoices
from api.models import Currency, Order from api.models import Currency, Order
@ -15,39 +15,84 @@ from tests.mocks.lnd import ( # MockRouterStub,; MockSignerStub,; MockVersioner
MockInvoicesStub, MockInvoicesStub,
MockLightningStub, MockLightningStub,
) )
from tests.test_api import BaseAPITestCase
class TradeTest(TestCase): def read_file(file_path):
"""
Read a file and return its content.
"""
with open(file_path, "r") as file:
return file.read()
class TradeTest(BaseAPITestCase):
su_pass = "12345678" su_pass = "12345678"
su_name = config("ESCROW_USERNAME", cast=str, default="admin") su_name = config("ESCROW_USERNAME", cast=str, default="admin")
def setUp(self): maker_form_with_range = {
"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,
}
@classmethod
def setUpTestData(cls):
""" """
Create a superuser. The superuser is the escrow party. Set up initial data for the test case.
""" """
self.client = Client() # Create super user
User.objects.create_superuser(self.su_name, "super@user.com", self.su_pass) User.objects.create_superuser(cls.su_name, "super@user.com", cls.su_pass)
# Fetch currency prices from external APIs
cache_market()
def test_login_superuser(self): def test_login_superuser(self):
""" """
Test logging in as a superuser. Test the login functionality for the superuser.
""" """
path = "/coordinator/login/" path = reverse("admin:login")
data = {"username": self.su_name, "password": self.su_pass} data = {"username": self.su_name, "password": self.su_pass}
response = self.client.post(path, data) response = self.client.post(path, data)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertResponse(response)
def test_cache_market(self):
"""
Test if the cache_market() call during test setup worked
"""
usd = Currency.objects.get(id=1)
self.assertIsInstance(
usd.exchange_rate,
Decimal,
f"Exchange rate is not a Decimal. Got {type(usd.exchange_rate)}",
)
self.assertGreater(
usd.exchange_rate, 0, "Exchange rate is not higher than zero"
)
self.assertIsInstance(
usd.timestamp, datetime, "External price timestamp is not a datetime"
)
def get_robot_auth(self, robot_index, first_encounter=False): 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 Create an AUTH header that embeds token, pub_key, and enc_priv_key into a single string
as requested by the robosats token middleware. as requested by the robosats token middleware.
""" """
with open(f"tests/robots/{robot_index}/b91_token", "r") as file:
b91_token = file.read() b91_token = read_file(f"tests/robots/{robot_index}/b91_token")
with open(f"tests/robots/{robot_index}/pub_key", "r") as file: pub_key = read_file(f"tests/robots/{robot_index}/pub_key")
pub_key = file.read() enc_priv_key = read_file(f"tests/robots/{robot_index}/enc_priv_key")
with open(f"tests/robots/{robot_index}/enc_priv_key", "r") as file:
enc_priv_key = file.read()
# First time a robot authenticated, it is registered by the backend, so pub_key and enc_priv_key is needed # First time a robot authenticated, it is registered by the backend, so pub_key and enc_priv_key is needed
if first_encounter: if first_encounter:
@ -59,14 +104,21 @@ class TradeTest(TestCase):
return headers, pub_key, enc_priv_key return headers, pub_key, enc_priv_key
def assert_robot(self, response, pub_key, enc_priv_key, expected_nickname): def assert_robot(self, response, pub_key, enc_priv_key, robot_index):
"""
Assert that the robot is created correctly.
"""
nickname = read_file(f"tests/robots/{robot_index}/nickname")
data = json.loads(response.content.decode()) data = json.loads(response.content.decode())
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertResponse(response)
self.assertEqual( self.assertEqual(
data["nickname"], data["nickname"],
expected_nickname, nickname,
"Robot created nickname is not MyopicRacket333", f"Robot created nickname is not {nickname}",
) )
self.assertEqual( self.assertEqual(
data["public_key"], pub_key, "Returned public Kky does not match" data["public_key"], pub_key, "Returned public Kky does not match"
@ -95,67 +147,37 @@ class TradeTest(TestCase):
""" """
Creates the robots in /tests/robots/{robot_index} Creates the robots in /tests/robots/{robot_index}
""" """
path = "/api/robot/" path = reverse("robot")
headers, pub_key, enc_priv_key = self.get_robot_auth(robot_index, True) headers, pub_key, enc_priv_key = self.get_robot_auth(robot_index, True)
response = self.client.get(path, **headers) response = self.client.get(path, **headers)
with open(f"tests/robots/{robot_index}/nickname", "r") as file: self.assert_robot(response, pub_key, enc_priv_key, robot_index)
expected_nickname = file.read()
self.assert_robot(response, pub_key, enc_priv_key, expected_nickname)
def test_create_robots(self): def test_create_robots(self):
""" """
Creates two robots to be used in the trade tests Test the creation of two robots to be used in the trade tests
""" """
self.create_robot(robot_index=1) self.create_robot(robot_index=1)
self.create_robot(robot_index=2) self.create_robot(robot_index=2)
def test_cache_market(self): def make_order(self, maker_form, robot_index=1):
cache_market() """
Create an order for the test.
usd = Currency.objects.get(id=1) """
self.assertIsInstance( path = reverse("make")
usd.exchange_rate,
Decimal,
f"Exchange rate is not a Decimal. Got {type(usd.exchange_rate)}",
)
self.assertGreater(
usd.exchange_rate, 0, "Exchange rate is not higher than zero"
)
self.assertIsInstance(
usd.timestamp, datetime, "External price timestamp is not a datetime"
)
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 # Get valid robot auth headers
headers, _, _ = self.get_robot_auth(robot_index, True) headers, _, _ = self.get_robot_auth(robot_index, True)
response = self.client.post(path, maker_form, **headers) response = self.client.post(path, maker_form, **headers)
return response return response
def test_create_order(self): def test_make_order(self):
maker_form = { """
"type": Order.Types.BUY, Test the creation of an order.
"currency": 1, """
"has_range": True, maker_form = self.maker_form_with_range
"min_amount": 21, response = self.make_order(maker_form, robot_index=1)
"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,
}
response = self.create_order(maker_form, robot_index=1)
data = json.loads(response.content.decode()) data = json.loads(response.content.decode())
# Checks # Checks
@ -237,7 +259,7 @@ class TradeTest(TestCase):
@patch("api.lightning.lnd.lightning_pb2_grpc.LightningStub", MockLightningStub) @patch("api.lightning.lnd.lightning_pb2_grpc.LightningStub", MockLightningStub)
@patch("api.lightning.lnd.invoices_pb2_grpc.InvoicesStub", MockInvoicesStub) @patch("api.lightning.lnd.invoices_pb2_grpc.InvoicesStub", MockInvoicesStub)
def get_order(self, order_id, robot_index=1, first_encounter=False): def get_order(self, order_id, robot_index=1, first_encounter=False):
path = "/api/order/" path = reverse("order")
params = f"?order_id={order_id}" params = f"?order_id={order_id}"
headers, _, _ = self.get_robot_auth(robot_index, first_encounter) headers, _, _ = self.get_robot_auth(robot_index, first_encounter)
response = self.client.get(path + params, **headers) response = self.client.get(path + params, **headers)
@ -246,22 +268,10 @@ class TradeTest(TestCase):
def test_get_order_created(self): def test_get_order_created(self):
# Make an order # Make an order
maker_form = { maker_form = self.maker_form_with_range
"type": Order.Types.BUY, robot_index = 1
"currency": 1,
"has_range": True, order_made_response = self.make_order(maker_form, robot_index)
"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()) order_made_data = json.loads(order_made_response.content.decode())
# Maker's first order fetch. Should trigger maker bond hold invoice generation. # Maker's first order fetch. Should trigger maker bond hold invoice generation.
@ -284,7 +294,9 @@ class TradeTest(TestCase):
self.assertEqual(data["status_message"], Order.Status(Order.Status.WFB).label) self.assertEqual(data["status_message"], Order.Status(Order.Status.WFB).label)
self.assertFalse(data["is_fiat_sent"]) self.assertFalse(data["is_fiat_sent"])
self.assertFalse(data["is_disputed"]) self.assertFalse(data["is_disputed"])
self.assertEqual(data["ur_nick"], "MyopicRacket333") self.assertEqual(
data["ur_nick"], read_file(f"tests/robots/{robot_index}/nickname")
)
self.assertTrue(isinstance(data["satoshis_now"], int)) self.assertTrue(isinstance(data["satoshis_now"], int))
self.assertFalse(data["maker_locked"]) self.assertFalse(data["maker_locked"])
self.assertFalse(data["taker_locked"]) self.assertFalse(data["taker_locked"])
@ -302,9 +314,9 @@ class TradeTest(TestCase):
follow_invoices = FollowInvoices() follow_invoices = FollowInvoices()
follow_invoices.follow_hold_invoices() follow_invoices.follow_hold_invoices()
def create_and_publish_order(self, maker_form, robot_index=1): def make_and_publish_order(self, maker_form, robot_index=1):
# Make an order # Make an order
order_made_response = self.create_order(maker_form, robot_index=1) order_made_response = self.make_order(maker_form, robot_index)
order_made_data = json.loads(order_made_response.content.decode()) order_made_data = json.loads(order_made_response.content.decode())
# Maker's first order fetch. Should trigger maker bond hold invoice generation. # Maker's first order fetch. Should trigger maker bond hold invoice generation.
@ -318,23 +330,9 @@ class TradeTest(TestCase):
return response return response
def test_publish_order(self): def test_publish_order(self):
maker_form = { maker_form = self.maker_form_with_range
"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 # Get order
response = self.create_and_publish_order(maker_form) response = self.make_and_publish_order(maker_form)
data = json.loads(response.content.decode()) data = json.loads(response.content.decode())
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -353,3 +351,66 @@ class TradeTest(TestCase):
self.assertFalse(public_data["is_participant"]) self.assertFalse(public_data["is_participant"])
self.assertTrue(isinstance(public_data["price_now"], float)) self.assertTrue(isinstance(public_data["price_now"], float))
self.assertTrue(isinstance(data["satoshis_now"], int)) self.assertTrue(isinstance(data["satoshis_now"], int))
@patch("api.lightning.cln.hold_pb2_grpc.HoldStub", MockHoldStub)
@patch("api.lightning.lnd.lightning_pb2_grpc.LightningStub", MockLightningStub)
@patch("api.lightning.lnd.invoices_pb2_grpc.InvoicesStub", MockInvoicesStub)
def take_order(self, order_id, amount, robot_index=2):
path = reverse("order")
params = f"?order_id={order_id}"
headers, _, _ = self.get_robot_auth(robot_index, first_encounter=True)
body = {"action": "take", "amount": amount}
response = self.client.post(path + params, body, **headers)
return response
def make_and_take_order(
self, maker_form, take_amount=80, maker_index=1, taker_index=2
):
response_published = self.make_and_publish_order(maker_form, maker_index)
data_publised = json.loads(response_published.content.decode())
response = self.take_order(data_publised["id"], take_amount, taker_index)
return response
# def test_make_and_take_order(self):
# maker_index = 1
# taker_index = 2
# maker_form = self.maker_form_with_range
# self.create_robot(taker_index) #### WEEEE SHOULD NOT BE NEEDED >??? WHY ROBOT HAS NO LOGIN TIME??
# response = self.make_and_take_order(maker_form, 80, maker_index, taker_index)
# data = json.loads(response.content.decode())
# print(data)
# self.assertEqual(
# data["ur_nick"], read_file(f"tests/robots/{taker_index}/nickname")
# )
# self.assertEqual(
# data["taker_nick"], read_file(f"tests/robots/{taker_index}/nickname")
# )
# self.assertEqual(
# data["maker_nick"], read_file(f"tests/robots/{maker_index}/nickname")
# )
# self.assertFalse(data["is_maker"])
# self.assertTrue(data["is_taker"])
# self.assertTrue(data["is_participant"])
# a = {
# "maker_status": "Active",
# "taker_status": "Active",
# "price_now": 38205.0,
# "premium_now": 3.34,
# "satoshis_now": 266196,
# "is_buyer": False,
# "is_seller": True,
# "taker_nick": "EquivalentWool707",
# "status_message": "Waiting for taker bond",
# "is_fiat_sent": False,
# "is_disputed": False,
# "ur_nick": "EquivalentWool707",
# "maker_locked": True,
# "taker_locked": False,
# "escrow_locked": False,
# "bond_invoice": "lntb73280n1pj5uypwpp5vklcx3s3c66ltz5v7kglppke5n3u6sa6h8m6whe278lza7rwfc7qd2j2pshjmt9de6zqun9vejhyetwvdjn5gp3vgcxgvfkv43z6e3cvyez6dpkxejj6cnxvsmj6c3exsuxxden89skzv3j9cs9g6rfwvs8qcted4jkuapq2ay5cnpqgefy2326g5syjn3qt984253q2aq5cnz92skzqcmgv43kkgr0dcs9ymmzdafkzarnyp5kvgr5dpjjqmr0vd4jqampwvs8xatrvdjhxumxw4kzugzfwss8w6tvdssxyefqw4hxcmmrddjkggpgveskjmpfyp6kumr9wdejq7t0w5sxx6r9v96zqmmjyp3kzmnrv4kzqatwd9kxzar9wfskcmre9ccqz2sxqzfvsp5hkz0dnvja244hc8jwmpeveaxtjd4ddzuqlpqc5zxa6tckr8py50s9qyyssqdcl6w2rhma7k3v904q4tuz68z82d6x47dgflk6m8jdtgt9dg3n9304axv8qvd66dq39sx7yu20sv5pyguv9dnjw3385y8utadxxsqtsqpf7p3w",
# "bond_satoshis": 7328,
# }