#![allow( clippy::tests_outside_test_module, clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, clippy::str_to_string, clippy::string_to_string, clippy::useless_format, clippy::items_after_test_module, clippy::too_many_arguments, clippy::doc_markdown, clippy::shadow_unrelated, clippy::use_debug, clippy::needless_as_bytes, clippy::missing_const_for_fn, unused_imports, unused_variables, unused_mut, dead_code, )] //! Mock FIX Server for Broker Gateway Testing //! //! Provides a lightweight FIX protocol server for integration testing //! without requiring external CQG connectivity. //! //! Features: //! - Session management (Logon/Logout) //! - Order acceptance (NewOrderSingle → ExecutionReport) //! - Latency simulation (configurable 10-20ms delays) //! - Error injection (rejections, disconnects, sequence gaps) //! - Message recording for assertion use anyhow::{Context, Result}; use std::collections::VecDeque; use std::sync::Arc; use std::time::Duration; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::RwLock; use tokio::time::sleep; /// FIX message representation (simplified for testing) #[derive(Debug, Clone)] pub struct FIXMessage { pub msg_type: String, // Tag 35 (e.g., "A" = Logon, "D" = NewOrderSingle) pub seq_num: u64, // Tag 34 pub sender_comp_id: String, // Tag 49 pub target_comp_id: String, // Tag 56 pub fields: Vec<(u16, String)>, // All FIX tags pub raw: String, // Raw FIX message } impl FIXMessage { /// Parse FIX message from raw string pub fn parse(raw: &str) -> Result { let mut fields = Vec::new(); let mut msg_type = String::new(); let mut seq_num = 0u64; let mut sender_comp_id = String::new(); let mut target_comp_id = String::new(); for field in raw.split('|') { if field.is_empty() { continue; } let parts: Vec<&str> = field.splitn(2, '=').collect(); if parts.len() != 2 { continue; } let tag: u16 = parts[0].parse().context("Invalid tag")?; let value = parts[1].to_string(); match tag { 35 => msg_type = value.clone(), 34 => seq_num = value.parse().context("Invalid seq_num")?, 49 => sender_comp_id = value.clone(), 56 => target_comp_id = value.clone(), _ => {} } fields.push((tag, value)); } Ok(Self { msg_type, seq_num, sender_comp_id, target_comp_id, fields, raw: raw.to_string(), }) } /// Get field value by tag pub fn get_field(&self, tag: u16) -> Option<&str> { self.fields .iter() .find(|(t, _)| *t == tag) .map(|(_, v)| v.as_str()) } /// Create Logon response pub fn create_logon_response(client_msg: &FIXMessage, seq_num: u64) -> String { format!( "8=FIX.4.2|9=100|35=A|34={}|49={}|56={}|98=0|108=30|10=123|", seq_num, client_msg.target_comp_id, client_msg.sender_comp_id ) } /// Create ExecutionReport (New acknowledgment) pub fn create_execution_report_new( client_order_id: &str, symbol: &str, side: &str, quantity: &str, seq_num: u64, sender: &str, target: &str, ) -> String { format!( "8=FIX.4.2|9=200|35=8|34={}|49={}|56={}|\ 37=BROKER_{}|11={}|17=EXEC_{}|20=0|150=0|39=0|\ 55={}|54={}|38={}|32=0|31=0|151={}|14=0|6=0|10=145|", seq_num, sender, target, &client_order_id[..8.min(client_order_id.len())], // BrokerOrderID client_order_id, &client_order_id[..8.min(client_order_id.len())], // ExecID symbol, side, quantity, quantity // LeavesQty ) } /// Create ExecutionReport (Fill) pub fn create_execution_report_fill( client_order_id: &str, broker_order_id: &str, symbol: &str, side: &str, quantity: &str, price: &str, seq_num: u64, sender: &str, target: &str, ) -> String { format!( "8=FIX.4.2|9=250|35=8|34={}|49={}|56={}|\ 37={}|11={}|17=EXEC_FILL_{}|20=0|150=F|39=2|\ 55={}|54={}|38={}|32={}|31={}|151=0|14={}|6={}|10=234|", seq_num, sender, target, broker_order_id, client_order_id, &client_order_id[..8.min(client_order_id.len())], symbol, side, quantity, quantity, // LastQty price, quantity, // CumQty price // AvgPx ) } /// Create ExecutionReport (Reject) pub fn create_execution_report_reject( client_order_id: &str, symbol: &str, reason: &str, seq_num: u64, sender: &str, target: &str, ) -> String { format!( "8=FIX.4.2|9=180|35=8|34={}|49={}|56={}|\ 37=BROKER_REJ|11={}|17=EXEC_REJ_{}|20=0|150=8|39=8|\ 55={}|58={}|10=089|", seq_num, sender, target, client_order_id, &client_order_id[..8.min(client_order_id.len())], symbol, reason ) } /// Create Heartbeat pub fn create_heartbeat(seq_num: u64, sender: &str, target: &str) -> String { format!( "8=FIX.4.2|9=60|35=0|34={}|49={}|56={}|10=089|", seq_num, sender, target ) } } /// Session state for mock server #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SessionState { Disconnected, Connected, LoggedIn, LoggedOut, } /// Mock FIX server for testing pub struct MockFIXServer { listener: Arc>>, session_state: Arc>, received_messages: Arc>>, response_queue: Arc>>, seq_num: Arc>, latency_ms: Arc>, reject_next_order: Arc>>, // Reject reason pub port: u16, pub sender_comp_id: String, pub target_comp_id: String, } impl MockFIXServer { /// Create and start mock FIX server on random port pub async fn start() -> Result { let listener = TcpListener::bind("127.0.0.1:0") .await .context("Failed to bind mock FIX server")?; let port = listener.local_addr()?.port(); let server = Self { listener: Arc::new(RwLock::new(Some(listener))), session_state: Arc::new(RwLock::new(SessionState::Disconnected)), received_messages: Arc::new(RwLock::new(Vec::new())), response_queue: Arc::new(RwLock::new(VecDeque::new())), seq_num: Arc::new(RwLock::new(1)), latency_ms: Arc::new(RwLock::new(0)), reject_next_order: Arc::new(RwLock::new(None)), port, sender_comp_id: "MOCK_CQG".to_string(), target_comp_id: "FOXHUNT_CLIENT".to_string(), }; Ok(server) } /// Accept incoming connection and handle FIX messages pub async fn accept_connection(&self) -> Result<()> { let listener_guard = self.listener.read().await; let listener = listener_guard .as_ref() .context("Listener not available")?; let (mut stream, _) = listener .accept() .await .context("Failed to accept connection")?; *self.session_state.write().await = SessionState::Connected; // Spawn handler task let session_state = self.session_state.clone(); let received_messages = self.received_messages.clone(); let seq_num = self.seq_num.clone(); let latency_ms = self.latency_ms.clone(); let reject_next_order = self.reject_next_order.clone(); let sender = self.sender_comp_id.clone(); let target = self.target_comp_id.clone(); tokio::spawn(async move { let mut buf = vec![0u8; 4096]; loop { match stream.read(&mut buf).await { Ok(0) => break, // Connection closed Ok(n) => { let raw = String::from_utf8_lossy(&buf[..n]).to_string(); // Parse FIX message if let Ok(msg) = FIXMessage::parse(&raw) { received_messages.write().await.push(msg.clone()); // Simulate latency let latency = *latency_ms.read().await; if latency > 0 { sleep(Duration::from_millis(latency)).await; } // Handle message based on type let response = match msg.msg_type.as_str() { "A" => { // Logon *session_state.write().await = SessionState::LoggedIn; let seq = *seq_num.read().await; *seq_num.write().await += 1; Some(FIXMessage::create_logon_response(&msg, seq)) } "D" => { // NewOrderSingle let seq = *seq_num.read().await; *seq_num.write().await += 1; // Check if rejection is configured let reject_reason = reject_next_order.write().await.take(); if let Some(reason) = reject_reason { // Send reject Some(FIXMessage::create_execution_report_reject( msg.get_field(11).unwrap_or("UNKNOWN"), msg.get_field(55).unwrap_or("UNKNOWN"), &reason, seq, &sender, &target, )) } else { // Send ExecutionReport (New) Some(FIXMessage::create_execution_report_new( msg.get_field(11).unwrap_or("UNKNOWN"), msg.get_field(55).unwrap_or("UNKNOWN"), msg.get_field(54).unwrap_or("1"), msg.get_field(38).unwrap_or("0"), seq, &sender, &target, )) } } "0" => { // Heartbeat - respond with heartbeat let seq = *seq_num.read().await; *seq_num.write().await += 1; Some(FIXMessage::create_heartbeat(seq, &sender, &target)) } "5" => { // Logout *session_state.write().await = SessionState::LoggedOut; None } _ => None, }; if let Some(resp) = response { if stream.write_all(resp.as_bytes()).await.is_err() { break; } } } } Err(_) => break, } } *session_state.write().await = SessionState::Disconnected; }); Ok(()) } /// Send fill execution report pub async fn send_fill( &self, client_order_id: &str, broker_order_id: &str, symbol: &str, side: &str, quantity: &str, price: &str, ) { let seq = *self.seq_num.read().await; *self.seq_num.write().await += 1; let msg = FIXMessage::create_execution_report_fill( client_order_id, broker_order_id, symbol, side, quantity, price, seq, &self.sender_comp_id, &self.target_comp_id, ); self.response_queue.write().await.push_back(msg); } /// Set latency simulation (milliseconds) pub async fn set_latency(&self, latency_ms: u64) { *self.latency_ms.write().await = latency_ms; } /// Simulate order rejection pub async fn reject_next_order(&self, reason: String) { *self.reject_next_order.write().await = Some(reason); } /// Get session state pub async fn get_session_state(&self) -> SessionState { *self.session_state.read().await } /// Get all received messages pub async fn get_received_messages(&self) -> Vec { self.received_messages.read().await.clone() } /// Clear received messages pub async fn clear_received_messages(&self) { self.received_messages.write().await.clear(); } /// Disconnect (close listener) pub async fn disconnect(&self) { *self.listener.write().await = None; *self.session_state.write().await = SessionState::Disconnected; } } #[cfg(test)] mod tests { use super::*; #[test] fn test_fix_message_parse_logon() { let raw = "8=FIX.4.2|9=120|35=A|34=1|49=FOXHUNT_CLIENT|56=CQG|98=0|108=30|10=123|"; let msg = FIXMessage::parse(raw).unwrap(); assert_eq!(msg.msg_type, "A"); assert_eq!(msg.seq_num, 1); assert_eq!(msg.sender_comp_id, "FOXHUNT_CLIENT"); assert_eq!(msg.target_comp_id, "CQG"); assert_eq!(msg.get_field(98), Some("0")); } #[test] fn test_fix_message_parse_new_order_single() { let raw = "8=FIX.4.2|9=180|35=D|34=2|49=CLIENT|56=CQG|11=ORDER123|55=ES|54=1|38=10|10=234|"; let msg = FIXMessage::parse(raw).unwrap(); assert_eq!(msg.msg_type, "D"); assert_eq!(msg.seq_num, 2); assert_eq!(msg.get_field(11), Some("ORDER123")); // ClOrdID assert_eq!(msg.get_field(55), Some("ES")); // Symbol assert_eq!(msg.get_field(54), Some("1")); // Side (Buy) assert_eq!(msg.get_field(38), Some("10")); // Quantity } #[test] fn test_create_logon_response() { let client_msg = FIXMessage { msg_type: "A".to_string(), seq_num: 1, sender_comp_id: "FOXHUNT_CLIENT".to_string(), target_comp_id: "CQG".to_string(), fields: vec![], raw: String::new(), }; let response = FIXMessage::create_logon_response(&client_msg, 1); assert!(response.contains("35=A")); // Logon assert!(response.contains("49=CQG")); // Sender swapped assert!(response.contains("56=FOXHUNT_CLIENT")); // Target swapped } #[test] fn test_create_execution_report_new() { let msg = FIXMessage::create_execution_report_new( "ORDER123", "ES", "1", "10", 5, "CQG", "FOXHUNT_CLIENT", ); assert!(msg.contains("35=8")); // ExecutionReport assert!(msg.contains("11=ORDER123")); // ClOrdID assert!(msg.contains("55=ES")); // Symbol assert!(msg.contains("150=0")); // ExecType=New assert!(msg.contains("39=0")); // OrdStatus=New } #[test] fn test_create_execution_report_fill() { let msg = FIXMessage::create_execution_report_fill( "ORDER123", "BROKER456", "ES", "1", "10", "5800.25", 10, "CQG", "FOXHUNT_CLIENT", ); assert!(msg.contains("35=8")); // ExecutionReport assert!(msg.contains("11=ORDER123")); // ClOrdID assert!(msg.contains("37=BROKER456")); // OrderID assert!(msg.contains("150=F")); // ExecType=Fill assert!(msg.contains("39=2")); // OrdStatus=Filled assert!(msg.contains("31=5800.25")); // LastPx } #[tokio::test] async fn test_mock_server_accept_logon() { let server = MockFIXServer::start().await.unwrap(); // Spawn accept task let server_clone = server.clone(); tokio::spawn(async move { server_clone.accept_connection().await.ok(); }); // Give server time to start tokio::time::sleep(Duration::from_millis(50)).await; // Connect as client let mut stream = TcpStream::connect(format!("127.0.0.1:{}", server.port)) .await .unwrap(); // Send Logon let logon = "8=FIX.4.2|9=120|35=A|34=1|49=FOXHUNT_CLIENT|56=CQG|98=0|108=30|10=123|"; stream.write_all(logon.as_bytes()).await.unwrap(); // Read response let mut buf = vec![0u8; 1024]; tokio::time::sleep(Duration::from_millis(100)).await; let n = stream.read(&mut buf).await.unwrap(); let response = String::from_utf8_lossy(&buf[..n]); assert!(response.contains("35=A")); // Logon response assert_eq!(server.get_session_state().await, SessionState::LoggedIn); } #[tokio::test] async fn test_mock_server_latency_simulation() { let server = MockFIXServer::start().await.unwrap(); server.set_latency(50).await; // 50ms latency let server_clone = server.clone(); tokio::spawn(async move { server_clone.accept_connection().await.ok(); }); tokio::time::sleep(Duration::from_millis(50)).await; let mut stream = TcpStream::connect(format!("127.0.0.1:{}", server.port)) .await .unwrap(); let logon = "8=FIX.4.2|9=120|35=A|34=1|49=FOXHUNT_CLIENT|56=CQG|98=0|108=30|10=123|"; let start = std::time::Instant::now(); stream.write_all(logon.as_bytes()).await.unwrap(); let mut buf = vec![0u8; 1024]; let _ = stream.read(&mut buf).await.unwrap(); let latency = start.elapsed(); // Should be at least 50ms due to latency simulation assert!(latency >= Duration::from_millis(45)); // Allow 5ms tolerance } #[tokio::test] async fn test_mock_server_reject_order() { let server = MockFIXServer::start().await.unwrap(); server .reject_next_order("Insufficient margin".to_string()) .await; let server_clone = server.clone(); tokio::spawn(async move { server_clone.accept_connection().await.ok(); }); tokio::time::sleep(Duration::from_millis(50)).await; let mut stream = TcpStream::connect(format!("127.0.0.1:{}", server.port)) .await .unwrap(); // Send NewOrderSingle let order = "8=FIX.4.2|9=180|35=D|34=2|49=CLIENT|56=CQG|11=ORDER123|55=ES|54=1|38=10|10=234|"; stream.write_all(order.as_bytes()).await.unwrap(); let mut buf = vec![0u8; 1024]; tokio::time::sleep(Duration::from_millis(100)).await; let n = stream.read(&mut buf).await.unwrap(); let response = String::from_utf8_lossy(&buf[..n]); assert!(response.contains("150=8")); // ExecType=Rejected assert!(response.contains("39=8")); // OrdStatus=Rejected assert!(response.contains("Insufficient margin")); } #[tokio::test] async fn test_mock_server_get_received_messages() { let server = MockFIXServer::start().await.unwrap(); let server_clone = server.clone(); tokio::spawn(async move { server_clone.accept_connection().await.ok(); }); tokio::time::sleep(Duration::from_millis(50)).await; let mut stream = TcpStream::connect(format!("127.0.0.1:{}", server.port)) .await .unwrap(); // Send Logon + Order let logon = "8=FIX.4.2|9=120|35=A|34=1|49=CLIENT|56=CQG|98=0|108=30|10=123|"; stream.write_all(logon.as_bytes()).await.unwrap(); tokio::time::sleep(Duration::from_millis(100)).await; let order = "8=FIX.4.2|9=180|35=D|34=2|49=CLIENT|56=CQG|11=ORDER123|55=ES|54=1|38=10|10=234|"; stream.write_all(order.as_bytes()).await.unwrap(); tokio::time::sleep(Duration::from_millis(100)).await; let messages = server.get_received_messages().await; assert_eq!(messages.len(), 2); assert_eq!(messages[0].msg_type, "A"); // Logon assert_eq!(messages[1].msg_type, "D"); // NewOrderSingle } } // Clone implementation for Arc-wrapped server impl Clone for MockFIXServer { fn clone(&self) -> Self { Self { listener: self.listener.clone(), session_state: self.session_state.clone(), received_messages: self.received_messages.clone(), response_queue: self.response_queue.clone(), seq_num: self.seq_num.clone(), latency_ms: self.latency_ms.clone(), reject_next_order: self.reject_next_order.clone(), port: self.port, sender_comp_id: self.sender_comp_id.clone(), target_comp_id: self.target_comp_id.clone(), } } }