from decimal import Decimal import logging import pytest from connection.tcp_connection_manager import TcpConnectionManager from proto.common_pb2 import Instrument, Side from tests.conftest import DEFAULT_PASSWORD from tests.common.auth_tests import AuthenticationTests from tests.common.mock_expectations import CallExpectationsManager from tests.test_client.admin_test_client import ( AdminClientConnectionHandlerFactory, AdminTestClient, ) from tests.test_client.risk_gateway_test_client import ( RiskGatewayClientConnectionHandlerFactory, RiskGatewayTestClient, ) logger = logging.getLogger(__name__) class TestExecutionSystem(AuthenticationTests): PROTOCOL = "execution" @pytest.fixture(autouse=True) def setup(self, request: pytest.FixtureRequest) -> None: self.test_name = request.node.name logger.info(f"Setting up test: {self.test_name}") self.tcp_connection_manager = TcpConnectionManager() self.call_expectations_manager = CallExpectationsManager() self.call_expectations_manager.setup_network(self.tcp_connection_manager) self._client_factory = RiskGatewayClientConnectionHandlerFactory(self.call_expectations_manager) self._admin_factory = AdminClientConnectionHandlerFactory(self.call_expectations_manager) self._admin_client: AdminTestClient | None = None self._next_instrument_id = 1 def _connect_unauthenticated(self) -> RiskGatewayTestClient: return self.tcp_connection_manager.connect(self.server_address, self._client_factory) def _connect_and_login(self, username: str | None = None) -> RiskGatewayTestClient: """Connect to the execution server, log in (if auth required), and return the client.""" client: RiskGatewayTestClient = self._connect_unauthenticated() if self.auth_required: client.test_login(username=username or self.test_name, password=DEFAULT_PASSWORD) return client def _get_admin_client(self) -> AdminTestClient: if self._admin_client is None: admin_address = self.orchestrator.get_server_address("admin") self._admin_client = self.tcp_connection_manager.connect( admin_address, self._admin_factory) return self._admin_client def _create_instrument_via_admin(self, tick_size: Decimal = Decimal("0.01")) -> str: """Create an instrument via admin and return its symbol.""" symbol = f"TEST.{self._next_instrument_id}" self._next_instrument_id += 1 instrument = Instrument( symbol=symbol, description="Test instrument", currency="USD", multiplier=1.0) admin = self._get_admin_client() admin.test_create_instrument(instrument, tick_size) return symbol # ========================================================================= # Authentication # ========================================================================= def test_insert_order_before_login_is_rejected(self) -> None: """Operations that require authentication must fail before login.""" if not self.auth_required: pytest.skip("Component does not require authentication") symbol = self._create_instrument_via_admin() client = self._connect_unauthenticated() client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10, expect_success=False) # ========================================================================= # Insert order # ========================================================================= def test_insert_order(self) -> None: symbol = self._create_instrument_via_admin() client = self._connect_and_login() resp = client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10) response = resp.get_response() assert response.order_id != 0, "InsertOrderResponse must include an order_id" assert response.timestamp != 0, "InsertOrderResponse must include a timestamp" def test_insert_order_on_unknown_instrument(self) -> None: self._create_instrument_via_admin() client = self._connect_and_login() client.test_insert_order("UNKNOWN.SYMBOL", Side.BUY, Decimal("100.0"), 10, expect_success=False) def test_insert_order_matching_produces_trade(self) -> None: symbol = self._create_instrument_via_admin() buyer = self._connect_and_login(f"{self.test_name}_2") seller = self._connect_and_login(f"{self.test_name}_3") buyer.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10) sell_resp = seller.test_insert_order(symbol, Side.SELL, Decimal("100.0"), 10) sell_response = sell_resp.get_response() assert len(sell_response.trade_ids) == 1, "Expected exactly one trade" assert sell_response.traded_quantity == 10 def test_orders_pass_without_any_limits_configured(self) -> None: """When no risk limits are set, orders must flow through without restriction.""" symbol = self._create_instrument_via_admin() client = self._connect_and_login() for _ in range(10): client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 1000) # ========================================================================= # Cancel order # ========================================================================= def test_cancel_resting_order(self) -> None: symbol = self._create_instrument_via_admin() client = self._connect_and_login() client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10) client.test_cancel_order(symbol) def test_cancel_nonexistent_order(self) -> None: symbol = self._create_instrument_via_admin() client = self._connect_and_login() client.test_cancel_order(symbol, order_id=999999, expect_success=False) def test_cancel_other_users_order_is_rejected(self) -> None: """A user must not be able to cancel another user's order.""" symbol = self._create_instrument_via_admin() client_a = self._connect_and_login(f"{self.test_name}_2") client_b = self._connect_and_login(f"{self.test_name}_3") resp_a = client_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10) order_id_a = resp_a.get_response().order_id client_b.test_cancel_order(symbol, order_id=order_id_a, expect_success=False)