From 9de14897150c7017ea7ad966bcf522f424e930b8 Mon Sep 17 00:00:00 2001 From: Deividas Petraitis Date: Tue, 10 Sep 2024 12:15:47 +0300 Subject: [PATCH] SQS-347 | integration, synthetic monitoring tests for active orders (#504) * SQS-347 | integration, synthetic monitoring tests for active orders query Implements integration, synthetic monitoring tests for the for active orders query. Additionally adds test file for locust. --- locust/locustfile.py | 2 +- locust/orderbook_active_orders.py | 18 --- locust/passthrough_orderbook_active_orders.py | 10 ++ tests/active_orderbook_orders_response.py | 111 ++++++++++++++++++ tests/sqs_service.py | 14 +++ tests/test_passthrough.py | 54 +++++++-- tests/test_synthetic_geo.py | 6 +- 7 files changed, 182 insertions(+), 33 deletions(-) delete mode 100644 locust/orderbook_active_orders.py create mode 100644 locust/passthrough_orderbook_active_orders.py create mode 100644 tests/active_orderbook_orders_response.py diff --git a/locust/locustfile.py b/locust/locustfile.py index 71a44b96c..1d2bbd810 100644 --- a/locust/locustfile.py +++ b/locust/locustfile.py @@ -4,5 +4,5 @@ from token_prices import TokenPrices from in_given_out import ExactAmountOutQuote from out_given_in import ExactAmountInQuote -from orderbook_active_orders import OrderbookActiveOrders from passthrough_portfolio_balances import PassthroughPortfolioBalances +from passthrough_orderbook_active_orders import PassthroughOrderbookActiveOrders diff --git a/locust/orderbook_active_orders.py b/locust/orderbook_active_orders.py deleted file mode 100644 index 644ce584d..000000000 --- a/locust/orderbook_active_orders.py +++ /dev/null @@ -1,18 +0,0 @@ -from locust import HttpUser, task -from pairs import * - -fill_bot_address = "osmo10s3vlv40h64qs2p98yal9w0tpm4r30uyg6ceux" - -class OrderbookActiveOrders(HttpUser): - # on_start is called when a Locust start before any task is scheduled. - def on_start(self): - pass - - # on_stop is called when the TaskSet is stopping - def on_stop(self): - pass - - - @task - def quoteUOSMOUSDC_1In(self): - self.client.get(f"passthrough/active-orders?userOsmoAddress={fill_bot_address}") diff --git a/locust/passthrough_orderbook_active_orders.py b/locust/passthrough_orderbook_active_orders.py new file mode 100644 index 000000000..498b80dd9 --- /dev/null +++ b/locust/passthrough_orderbook_active_orders.py @@ -0,0 +1,10 @@ +from locust import HttpUser, task + +# burner address for the integration tests +addr = "osmo1jgz4xmaw9yk9pjxd4h8c2zs0r0vmgyn88s8t6l" + + +class PassthroughOrderbookActiveOrders(HttpUser): + @task + def passthrough_orderbook_active_orders(self): + self.client.get(f"/passthrough/active-orders?userOsmoAddress={addr}") diff --git a/tests/active_orderbook_orders_response.py b/tests/active_orderbook_orders_response.py new file mode 100644 index 000000000..a474b055a --- /dev/null +++ b/tests/active_orderbook_orders_response.py @@ -0,0 +1,111 @@ +from decimal import Decimal +from typing import List +import time +from datetime import datetime + + +class LimitOrderAsset: + def __init__(self, symbol: str): + self.symbol = symbol + + def validate(self): + # Ensure symbol is not empty + assert self.symbol, "Symbol must not be empty" + +class LimitOrder: + def __init__(self, tick_id: int, order_id: int, order_direction: str, owner: str, quantity: str, etas: str, + claim_bounty: str, placed_quantity: str, placed_at: int, price: str, percentClaimed: str, + totalFilled: str, percentFilled: str, orderbookAddress: str, status: str, output: str, + quote_asset: dict, base_asset: dict): + self.tick_id = int(tick_id) + self.order_id = int(order_id) + self.order_direction = order_direction + self.owner = owner + self.quantity = Decimal(quantity) + self.etas = Decimal(etas) + self.claim_bounty = Decimal(claim_bounty) + self.placed_quantity = Decimal(placed_quantity) + self.placed_at = int(placed_at) + self.price = Decimal(price) + self.percent_claimed = Decimal(percentClaimed) # Changed variable name + self.total_filled = Decimal(totalFilled) + self.percent_filled = Decimal(percentFilled) + self.orderbook_address = orderbookAddress + self.status = status + self.output = Decimal(output) + self.quote_asset = LimitOrderAsset(**quote_asset) + self.base_asset = LimitOrderAsset(**base_asset) + + def validate(self, owner_address=None): + # Check if order_id is non-negative + assert self.order_id >= 0, f"Order ID {self.order_id} cannot be negative" + + # Check if order_direction is either "bid" or "ask" + assert self.order_direction in ['bid', 'ask'], f"Order direction {self.order_direction} must be 'bid' or 'ask'" + + # Validate owner address (Osmosis address format) + assert self.owner == owner_address, f"Owner address {self.owner} is invalid" + + # Check if quantity is non-negative + assert self.quantity > 0, f"Quantity {self.quantity} cannot be negative" + + # Check if claim_bounty is non-negative + assert self.claim_bounty > 0, f"Claim bounty {self.claim_bounty} cannot be negative" + + # Validate placed_quantity is non-negative + assert self.placed_quantity > 0, f"Placed quantity {self.placed_quantity} cannot be negative" + + # Validate placed_at is a valid Unix timestamp + assert 0 <= self.placed_at <= int(time.time()), f"Placed_at timestamp {self.placed_at} is invalid" + + # Check if price is positive + assert self.price > 0, f"Price {self.price} must be positive" + + # Check if percent_claimed is between 0 and 100 + assert 0 <= self.percent_claimed <= 100, f"Percent claimed {self.percent_claimed} must be between 0 and 100" + + # Check if total_filled is non-negative + assert self.total_filled >= 0, f"Total filled {self.total_filled} cannot be negative" + + # Check if percent_filled is between 0 and 100 + assert 0 <= self.percent_filled <= 100, f"Percent filled {self.percent_filled} must be between 0 and 100" + + # Ensure status is not empty + assert self.status, "Status must not be empty" + + # Ensure orderbook_address is not empty + assert self.orderbook_address, "Orderbook address must not be empty" + + # Check if output is non-negative + assert self.output >= 0, f"Output {self.output} cannot be negative" + + # Validate quote_asset + self.quote_asset.validate() + + # Validate base_asset + self.base_asset.validate() + + @staticmethod + def _is_valid_unix_timestamp(timestamp): + try: + datetime.utcfromtimestamp(int(timestamp)) + return True + except (ValueError, OverflowError): + return False + + +class OrderbookActiveOrdersResponse: + def __init__(self, orders: List[dict], is_best_effort: bool): + self.orders = [LimitOrder(**order) for order in orders] + self.is_best_effort = is_best_effort + + def validate(self, owner_address): + # Validate each order + order_ids = set() + for order in self.orders: + order.validate(owner_address) + + # Ensure order_id is unique + if order.order_id in order_ids: + raise ValueError(f"Duplicate order_id found: {order.order_id}") + order_ids.add(order.order_id) diff --git a/tests/sqs_service.py b/tests/sqs_service.py index b40442d51..8657f3886 100644 --- a/tests/sqs_service.py +++ b/tests/sqs_service.py @@ -15,6 +15,7 @@ CANONICAL_ORDERBOOKS_URL = "/pools/canonical-orderbooks" PASSTHROUGH_PORTFOLIO_ASSETS = "/passthrough/portfolio-assets/" +PASSTHROUGH_ACTIVE_ORDERBOOK_ORDERS = "/passthrough/active-orders" CONFIG_URL = "/config" @@ -240,6 +241,19 @@ def get_canonical_orderbooks(self): return response.json() + + def get_active_orderbook_orders(self, address): + """ + Fetches active orderbook orders from the specified endpoint and address and returns them. + """ + + response = requests.get(self.url + f"{PASSTHROUGH_ACTIVE_ORDERBOOK_ORDERS}?userOsmoAddress={address}", headers=self.headers) + + if response.status_code != 200: + raise Exception(f"Error fetching active orderbook orders: {response.text}") + + return response.json() + def get_portfolio_assets(self, address): """ Fetches the portfolio assets from the specified endpoint and address and returns them. diff --git a/tests/test_passthrough.py b/tests/test_passthrough.py index be4c2d6ac..ce9821f99 100644 --- a/tests/test_passthrough.py +++ b/tests/test_passthrough.py @@ -1,34 +1,63 @@ +import time import conftest import pytest from sqs_service import * -import util +from active_orderbook_orders_response import OrderbookActiveOrdersResponse from conftest import SERVICE_MAP from e2e_math import * from decimal import * +# Arbitrary choice based on performance at the time of test writing +EXPECTED_LATENCY_UPPER_BOUND_MS = 150 -user_balances_assets_category_name = "user-balances" -unstaking_assets_category_name = "unstaking" -staked_assets_category_name = "staked" -inLocks_assets_category_name = "in-locks" -pooled_assets_category_name = "pooled" -unclaimed_rewards_assets_category_name = "unclaimed-rewards" -total_assets_category_name = "total-assets" +user_balances_assets_category_name = "user-balances" +unstaking_assets_category_name = "unstaking" +staked_assets_category_name = "staked" +inLocks_assets_category_name = "in-locks" +pooled_assets_category_name = "pooled" +unclaimed_rewards_assets_category_name = "unclaimed-rewards" +total_assets_category_name = "total-assets" # Test suite for the /passthrough endpoint # Note: this is for convinience to skip long-running tests in development # locally. # @pytest.mark.skip(reason="This test is currently disabled") -class TestPassthrough: + +class TestPassthrough: def test_poortfolio_assets(self, environment_url): run_test_portfolio_assets(environment_url) + def test_active_orderbook_orders(self, environment_url): + run_test_active_orderbook_orders(environment_url) + + +def run_test_active_orderbook_orders(environment_url): + sqs_service = SERVICE_MAP[environment_url] + + # list of burner addresses for the integration tests + addresses = [ + "osmo1jgz4xmaw9yk9pjxd4h8c2zs0r0vmgyn88s8t6l", + ] + + for address in addresses: + + start_time = time.time() + response = sqs_service.get_active_orderbook_orders(address) + elapsed_time_ms = (time.time() - start_time) * 1000 + + assert EXPECTED_LATENCY_UPPER_BOUND_MS > elapsed_time_ms, f"Error: latency {elapsed_time_ms} exceeded {EXPECTED_LATENCY_UPPER_BOUND_MS} ms" + + resp = OrderbookActiveOrdersResponse(**response) + + resp.validate(address) + + def run_test_portfolio_assets(environment_url): sqs_service = SERVICE_MAP[environment_url] - + # Arbitrary addresses addresses = [ "osmo1044qatzg4a0wm63jchrfdnn2u8nwdgxxt6e524", @@ -64,6 +93,7 @@ def run_test_portfolio_assets(environment_url): total_assets = categories.get(total_assets_category_name) validate_category(total_assets, True) + def validate_category(category, should_have_breakdown=False): assert category is not None @@ -80,9 +110,7 @@ def validate_category(category, should_have_breakdown=False): account_coins_result = category.get('account_coins_result') assert account_coins_result is not None - + for coin_result in account_coins_result: assert coin_result.get('coin') is not None assert coin_result.get('cap_value') is not None - - diff --git a/tests/test_synthetic_geo.py b/tests/test_synthetic_geo.py index a4040645a..b0a13e8da 100644 --- a/tests/test_synthetic_geo.py +++ b/tests/test_synthetic_geo.py @@ -8,7 +8,7 @@ from util import * from test_pools import run_pool_liquidity_cap_test, run_canonical_orderbook_test, run_pool_filters_test -from test_passthrough import run_test_portfolio_assets +from test_passthrough import run_test_portfolio_assets, run_test_active_orderbook_orders from test_candidate_routes import run_candidate_routes_test from test_router_quote_out_given_in import TestExactAmountInQuote from test_router_quote_in_given_out import TestExactAmountOutQuote @@ -41,6 +41,10 @@ def test_synth_pools_filters(self, environment_url): def test_synth_passthrough_portfolio_assets(self, environment_url): run_test_portfolio_assets(environment_url) + # /passthrough/active-orders endpoint + def test_synth_passthrough_active_orders(self, environment_url): + run_test_active_orderbook_orders(environment_url) + # /router/routes endpoint def test_synth_candidate_routes(self, environment_url): tokens_to_pair = [constants.USDC, constants.UOSMO]