Repository: wboayue/rust-ibapi
Pull request #232: connection bus integration tests
Author: login: fhill2
Created: 2025-03-25T03:12:07Z | Merged: 2025-04-15T23:19:01Z

I noticed there is limited test coverage for the MessageBus, Connection and Dispatcher thread, and wanted to share an implementation that can improve the coverage.

Here is an implementation (WIP) similar to what i'm using to perform integration tests against my own client.

TWS / Gateway is mocked and run on a thread, The IB reconnect routine is simulated and supports returning responses when requests are received.  The behaviour of the server can be configured at the start of the test by declaring the request & responses vectors similar how the `MessageBusStub` can be configured for each test.

The  difference between this implementation and the current `MessageBusStub`  is the ability to schedule the handshake, and closing the socket.

```rust
   let messages = VecDeque::from(vec![
        Message::handshake("API\0\0\0\0\tv100..173", vec!["173\020250323 22:21:01 Greenwich Mean Time\0"]),
        Message::packet("71\02\028\0\0", vec!["15\01\0DU1234567\0", "9\01\01\0"]),
        // do not send a response back to keep the client waiting...
        Message::packet("9\08\09000\00\0AAPL\0STK\0\00\0\0\0SMART\0\0USD\0\0\00\0\0\0", vec![]),
        Message::Eof,
        Message::handshake("API\0\0\0\0\tv100..173", vec!["173\020250323 22:21:01 Greenwich Mean Time\0"]),
        Message::packet("71\02\028\0\0", vec!["15\01\0DU1234567\0", "9\01\01\0"]),
        Message::packet(
            "9\08\09001\00\0AAPL\0STK\0\00\0\0\0SMART\0\0USD\0\0\00\0\0\0",
            vec![&format!("10\09001\0{}", AAPL_CONTRACT), "52\01\09001\0"],
        ),
    ]);
```

By structuring the tests this way the behaviour of the client before, during, after a disconnect can be tested. As the server or stream is mocked instead of the MessageBus, all modules are tested.

Passing  a mocked TcpStream to the Connection then performing tests can be another solution.

Any thoughts on the best approach to test these modules, and if this implementation can work for the library?


Diff:
diff --git a/src/client.rs b/src/client.rs
index 2fc1d3b2..637757f0 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -1,5 +1,6 @@
 use std::fmt::Debug;
 use std::marker::PhantomData;
+use std::net::TcpStream;
 use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
 use std::sync::{Arc, Mutex};
 use std::time::Duration;
@@ -19,7 +20,7 @@ use crate::messages::{RequestMessage, ResponseMessage};
 use crate::news::NewsArticle;
 use crate::orders::{CancelOrder, Executions, ExerciseOptions, Order, Orders, PlaceOrder};
 use crate::scanner::ScannerData;
-use crate::transport::{Connection, ConnectionMetadata, InternalSubscription, MessageBus, TcpMessageBus};
+use crate::transport::{Connection, ConnectionMetadata, InternalSubscription, MessageBus, TcpMessageBus, TcpSocket};
 use crate::wsh::AutoFill;
 use crate::{accounts, contracts, market_data, news, orders, scanner, wsh};
 
@@ -64,7 +65,10 @@ impl Client {
     /// println!("next_order_id: {}", client.next_order_id());
     /// ```
     pub fn connect(address: &str, client_id: i32) -> Result<Client, Error> {
-        let connection = Connection::connect(client_id, address)?;
+        let stream = TcpStream::connect(address)?;
+        let socket = TcpSocket::new(stream, address)?;
+
+        let connection = Connection::connect(socket, client_id)?;
         let connection_metadata = connection.connection_metadata();
 
         let message_bus = Arc::new(TcpMessageBus::new(connection)?);
diff --git a/src/contracts.rs b/src/contracts.rs
index 318df19e..e67fa2d9 100644
--- a/src/contracts.rs
+++ b/src/contracts.rs
@@ -19,7 +19,7 @@ use crate::Client;
 use crate::{server_versions, Error, ToField};
 
 pub(crate) mod decoders;
-mod encoders;
+pub(crate) mod encoders;
 pub mod tick_types;
 
 #[cfg(test)]
diff --git a/src/messages.rs b/src/messages.rs
index 57796c1f..b17fa409 100644
--- a/src/messages.rs
+++ b/src/messages.rs
@@ -1,7 +1,10 @@
 use std::fmt::Display;
+use std::io::Write;
 use std::ops::Index;
 use std::str::{self, FromStr};
 
+use byteorder::{BigEndian, WriteBytesExt};
+
 use log::debug;
 use serde::{Deserialize, Serialize};
 use time::OffsetDateTime;
@@ -349,9 +352,19 @@ impl ToField for OutgoingMessages {
     }
 }
 
+pub fn encode_length(message: &str) -> Vec<u8> {
+    let data = message.as_bytes();
+
+    let mut packet: Vec<u8> = Vec::with_capacity(data.len() + 4);
+
+    packet.write_u32::<BigEndian>(data.len() as u32).unwrap();
+    packet.write_all(data).unwrap();
+    packet
+}
+
 #[derive(Default, Debug, Clone)]
 pub(crate) struct RequestMessage {
-    fields: Vec<String>,
+    pub(crate) fields: Vec<String>,
 }
 
 impl RequestMessage {
@@ -382,6 +395,18 @@ impl RequestMessage {
         data.push('|');
         data
     }
+    #[cfg(test)]
+    pub fn from(fields: &str) -> RequestMessage {
+        RequestMessage {
+            fields: fields.split('\x00').map(|x| x.to_string()).collect(),
+        }
+    }
+    #[cfg(test)]
+    pub fn from_simple(fields: &str) -> RequestMessage {
+        RequestMessage {
+            fields: fields.split('|').map(|x| x.to_string()).collect(),
+        }
+    }
 }
 
 impl Index<usize> for RequestMessage {
@@ -605,11 +630,12 @@ impl ResponseMessage {
             fields: fields.split('\x00').map(|x| x.to_string()).collect(),
         }
     }
-
     #[cfg(test)]
     pub fn from_simple(fields: &str) -> ResponseMessage {
-        let fields = fields.replace("|", "\0");
-        Self::from(&fields)
+        ResponseMessage {
+            i: 0,
+            fields: fields.split('|').map(|x| x.to_string()).collect(),
+        }
     }
 
     pub fn skip(&mut self) {
diff --git a/src/orders.rs b/src/orders.rs
index c29ebb61..20809b66 100644
--- a/src/orders.rs
+++ b/src/orders.rs
@@ -13,7 +13,7 @@ use crate::{encode_option_field, ToField};
 use crate::{server_versions, Error};
 
 mod decoders;
-mod encoders;
+pub(crate) mod encoders;
 #[cfg(test)]
 mod tests;
 
diff --git a/src/transport.rs b/src/transport.rs
index bdb923bf..d138a3bb 100644
--- a/src/transport.rs
+++ b/src/transport.rs
@@ -3,7 +3,7 @@
 //! and responses from TWS back to the Client.
 
 use std::collections::HashMap;
-use std::io::{prelude::*, Cursor, ErrorKind};
+use std::io::{self, prelude::*, Cursor, ErrorKind};
 use std::net::TcpStream;
 use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::{Arc, Mutex, RwLock};
@@ -17,10 +17,11 @@ use time::macros::format_description;
 use time::OffsetDateTime;
 use time_tz::{timezones, OffsetResult, PrimitiveDateTimeExt, Tz};
 
-use crate::messages::{shared_channel_configuration, IncomingMessages, OutgoingMessages, RequestMessage, ResponseMessage};
+use crate::messages::{encode_length, shared_channel_configuration, IncomingMessages, OutgoingMessages, RequestMessage, ResponseMessage};
 use crate::{server_versions, Error};
 use recorder::MessageRecorder;
 
+mod connection;
 mod recorder;
 
 const MIN_SERVER_VERSION: i32 = 100;
@@ -144,8 +145,8 @@ pub enum Signal {
 }
 
 #[derive(Debug)]
-pub struct TcpMessageBus {
-    connection: Connection,
+pub struct TcpMessageBus<S: Stream> {
+    connection: Connection<S>,
     handles: Mutex<Vec<JoinHandle<()>>>,
     requests: SenderHash<i32, Response>,
     orders: SenderHash<i32, Response>,
@@ -156,8 +157,8 @@ pub struct TcpMessageBus {
     shutdown_requested: AtomicBool,
 }
 
-impl TcpMessageBus {
-    pub fn new(connection: Connection) -> Result<TcpMessageBus, Error> {
+impl<S: Stream> TcpMessageBus<S> {
+    pub fn new(connection: Connection<S>) -> Result<TcpMessageBus<S>, Error> {
         let (signals_send, signals_recv) = channel::unbounded();
 
         Ok(TcpMessageBus {
@@ -216,48 +217,58 @@ impl TcpMessageBus {
     fn read_message(&self) -> Response {
         self.connection.read_message()
     }
+    fn dispatch(&self, server_version: i32) -> Result<(), Error> {
+        const RECONNECT_CODES: &[ErrorKind] = &[ErrorKind::ConnectionReset, ErrorKind::ConnectionAborted, ErrorKind::UnexpectedEof];
+        const TIMEOUT_CODES: &[ErrorKind] = &[ErrorKind::WouldBlock, ErrorKind::TimedOut];
+        match self.read_message() {
+            Ok(message) => {
+                self.dispatch_message(server_version, message);
+                Ok(())
+            }
+            Err(Error::Io(e)) if TIMEOUT_CODES.contains(&e.kind()) => {
+                if self.is_shutting_down() {
+                    debug!("dispatcher thread exiting");
+                    return Err(Error::Shutdown);
+                }
+                Ok(())
+            }
+            Err(Error::Io(e)) if RECONNECT_CODES.contains(&e.kind()) => {
+                error!("error reading next message (will attempt reconnect): {:?}", e);
+
+                if let Err(reconnect_err) = self.connection.reconnect() {
+                    error!("failed to reconnect to TWS/Gateway: {:?}", reconnect_err);
+                    self.request_shutdown();
+                    return Err(Error::ConnectionFailed);
+                }
+
+                info!("successfully reconnected to TWS/Gateway");
+                self.reset();
+                Ok(())
+            }
+            Err(err) => {
+                error!("error reading next message (shutting down): {:?}", err);
+                self.request_shutdown();
+                Err(err)
+            }
+        }
+    }
 
     // Dispatcher thread reads messages from TWS and dispatches them to
     // appropriate channel.
     fn start_dispatcher_thread(self: &Arc<Self>, server_version: i32) -> JoinHandle<()> {
         let message_bus = Arc::clone(self);
-
-        const RECONNECT_CODES: &[ErrorKind] = &[ErrorKind::ConnectionReset, ErrorKind::ConnectionAborted, ErrorKind::UnexpectedEof];
-        const TIMEOUT_CODES: &[ErrorKind] = &[ErrorKind::WouldBlock, ErrorKind::TimedOut];
-
         thread::spawn(move || {
             loop {
-                match message_bus.read_message() {
-                    Ok(message) => {
-                        message_bus.dispatch_message(server_version, message);
-                    }
-                    Err(Error::Io(e)) if TIMEOUT_CODES.contains(&e.kind()) => {
-                        if message_bus.is_shutting_down() {
-                            debug!("dispatcher thread exiting");
-                            return;
-                        }
-                    }
-                    Err(Error::Io(e)) if RECONNECT_CODES.contains(&e.kind()) => {
-                        error!("error reading next message (will attempt reconnect): {:?}", e);
-
-                        // Attempt to reconnect to TWS.
-                        if let Err(e) = message_bus.connection.reconnect() {
-                            error!("failed to reconnect to TWS/Gateway: {:?}", e);
-                            message_bus.request_shutdown();
-                            return;
-                        }
-
-                        info!("successfully reconnected to TWS/Gateway");
-                        message_bus.reset();
-                        continue;
-                    }
-                    Err(err) => {
-                        error!("error reading next message (shutting down): {:?}", err);
-                        message_bus.request_shutdown();
-                        return;
+                match message_bus.dispatch(server_version) {
+                    Ok(_) => continue,
+                    Err(Error::Shutdown) | Err(Error::ConnectionFailed) => break,
+                    Err(e) => {
+                        error!("Dispatcher encountered an error: {:?}", e);
+                        break;
                     }
-                };
+                }
             }
+            debug!("Dispatcher thread finished.");
         })
     }
 
@@ -422,14 +433,14 @@ impl TcpMessageBus {
 
 const UNSPECIFIED_REQUEST_ID: i32 = -1;
 
-impl MessageBus for TcpMessageBus {
-    fn send_request(&self, request_id: i32, packet: &RequestMessage) -> Result<InternalSubscription, Error> {
+impl<S: Stream> MessageBus for TcpMessageBus<S> {
+    fn send_request(&self, request_id: i32, message: &RequestMessage) -> Result<InternalSubscription, Error> {
         let (sender, receiver) = channel::unbounded();
         let sender_copy = sender.clone();
 
         self.requests.insert(request_id, sender);
 
-        self.connection.write_message(packet)?;
+        self.connection.write_message(message)?;
 
         let subscription = SubscriptionBuilder::new()
             .receiver(receiver)
@@ -508,16 +519,6 @@ impl MessageBus for TcpMessageBus {
     }
 }
 
-fn read_header(mut reader: &TcpStream) -> Result<usize, Error> {
-    let buffer = &mut [0_u8; 4];
-    reader.read_exact(buffer)?;
-
-    let mut reader = Cursor::new(buffer);
-    let count = reader.read_u32::<BigEndian>()?;
-
-    Ok(count as usize)
-}
-
 fn error_event(server_version: i32, mut packet: ResponseMessage) -> Result<(), Error> {
     packet.skip(); // message_id
 
@@ -789,28 +790,102 @@ pub(crate) struct ConnectionMetadata {
 }
 
 #[derive(Debug)]
-pub(crate) struct Connection {
-    client_id: i32,
-    connection_url: String,
+pub(crate) struct TcpSocket {
     reader: Mutex<TcpStream>,
     writer: Mutex<TcpStream>,
+    connection_url: String,
+}
+impl TcpSocket {
+    pub fn new(stream: TcpStream, connection_url: &str) -> Result<Self, Error> {
+        let writer = stream.try_clone()?;
+
+        stream.set_read_timeout(Some(TWS_READ_TIMEOUT))?;
+
+        Ok(Self {
+            reader: Mutex::new(stream),
+            writer: Mutex::new(writer),
+            connection_url: connection_url.to_string(),
+        })
+    }
+}
+
+impl Reconnect for TcpSocket {
+    fn reconnect(&self) -> Result<(), Error> {
+        match TcpStream::connect(&self.connection_url) {
+            Ok(stream) => {
+                stream.set_read_timeout(Some(TWS_READ_TIMEOUT))?;
+
+                let mut reader = self.reader.lock()?;
+                *reader = stream.try_clone()?;
+
+                let mut writer = self.writer.lock()?;
+                *writer = stream;
+
+                Ok(())
+            }
+            Err(e) => Err(e.into()),
+        }
+    }
+    fn sleep(&self, duration: std::time::Duration) {
+        thread::sleep(duration)
+    }
+}
+
+pub(crate) trait Reconnect {
+    fn reconnect(&self) -> Result<(), Error>;
+    fn sleep(&self, duration: std::time::Duration);
+}
+
+pub(crate) trait Stream: Io + Reconnect + Sync + Send + 'static + std::fmt::Debug {}
+impl Stream for TcpSocket {}
+
+fn read_header(reader: &mut impl Read) -> Result<usize, Error> {
+    let buffer = &mut [0_u8; 4];
+    reader.read_exact(buffer)?;
+    let mut reader = Cursor::new(buffer);
+    let count = reader.read_u32::<BigEndian>()?;
+    Ok(count as usize)
+}
+
+fn read_message(reader: &mut impl Read) -> Result<Vec<u8>, Error> {
+    let message_size = read_header(reader)?;
+    let mut data = vec![0_u8; message_size];
+    reader.read_exact(&mut data)?;
+    Ok(data)
+}
+
+impl Io for TcpSocket {
+    fn read_message(&self) -> Result<Vec<u8>, Error> {
+        let mut reader = self.reader.lock()?;
+        Ok(read_message(&mut *reader)?)
+    }
+
+    fn write_all(&self, buf: &[u8]) -> Result<(), Error> {
+        let mut writer = self.writer.lock()?;
+        writer.write(buf)?;
+        Ok(())
+    }
+}
+
+pub(crate) trait Io {
+    fn read_message(&self) -> Result<Vec<u8>, Error>;
+    fn write_all(&self, buf: &[u8]) -> Result<(), Error>;
+}
+
+#[derive(Debug)]
+pub(crate) struct Connection<S: Stream> {
+    client_id: i32,
+    socket: S,
     connection_metadata: Mutex<ConnectionMetadata>,
     max_retries: i32,
     recorder: MessageRecorder,
 }
 
-impl Connection {
-    pub fn connect(client_id: i32, connection_url: &str) -> Result<Self, Error> {
-        let reader = TcpStream::connect(connection_url)?;
-        let writer = reader.try_clone()?;
-
-        reader.set_read_timeout(Some(TWS_READ_TIMEOUT))?;
-
+impl<S: Stream> Connection<S> {
+    pub fn connect(socket: S, client_id: i32) -> Result<Self, Error> {
         let connection = Self {
             client_id,
-            connection_url: connection_url.into(),
-            reader: Mutex::new(reader),
-            writer: Mutex::new(writer),
+            socket,
             connection_metadata: Mutex::new(ConnectionMetadata {
                 client_id,
                 ..Default::default()
@@ -836,20 +911,10 @@ impl Connection {
             let next_delay = backoff.next_delay();
             info!("next reconnection attempt in {next_delay:#?}");
 
-            thread::sleep(next_delay);
-
-            match TcpStream::connect(&self.connection_url) {
-                Ok(stream) => {
-                    {
-                        let mut reader = self.reader.lock()?;
-                        let mut writer = self.writer.lock()?;
-
-                        *reader = stream.try_clone()?;
-                        reader.set_read_timeout(Some(TWS_READ_TIMEOUT))?;
-
-                        *writer = stream;
-                    }
+            self.socket.sleep(next_delay);
 
+            match self.socket.reconnect() {
+                Ok(_) => {
                     info!("reconnected !!!");
                     self.establish_connection()?;
 
@@ -871,44 +936,22 @@ impl Connection {
         Ok(())
     }
 
-    fn write(&self, data: &str) -> Result<(), Error> {
-        let mut writer = self.writer.lock()?;
-        writer.write_all(data.as_bytes())?;
-        Ok(())
-    }
-
     fn write_message(&self, message: &RequestMessage) -> Result<(), Error> {
-        let mut writer = self.writer.lock()?;
-
-        let data = message.encode();
-        debug!("-> {data:?}");
-
-        let data = data.as_bytes();
-
-        let mut packet = Vec::with_capacity(data.len() + 4);
-
-        packet.write_u32::<BigEndian>(data.len() as u32)?;
-        packet.write_all(data)?;
-
-        writer.write_all(&packet)?;
-
         self.recorder.record_request(message);
-
+        let encoded = message.encode();
+        debug!("-> {encoded:?}");
+        let length_encoded = encode_length(&encoded);
+        self.socket.write_all(&length_encoded)?;
         Ok(())
     }
 
     fn read_message(&self) -> Response {
-        let mut reader = self.reader.lock()?;
-
-        let message_size = read_header(&reader)?;
-        let mut data = vec![0_u8; message_size];
-
-        reader.read_exact(&mut data)?;
-
+        let data = self.socket.read_message()?;
         let raw_string = String::from_utf8(data)?;
         debug!("<- {:?}", raw_string);
 
         let message = ResponseMessage::from(&raw_string);
+
         self.recorder.record_response(&message);
 
         Ok(message)
@@ -916,11 +959,14 @@ impl Connection {
 
     // sends server handshake
     fn handshake(&self) -> Result<(), Error> {
-        let prefix = "API\0";
-        let version = format!("v{MIN_SERVER_VERSION}..{MAX_SERVER_VERSION}");
+        let version = &format!("v{MIN_SERVER_VERSION}..{MAX_SERVER_VERSION}");
+
+        debug!("-> {version:?}");
+
+        let mut handshake = Vec::from(b"API\0");
+        handshake.extend_from_slice(&encode_length(version));
 
-        let packet = prefix.to_owned() + &encode_packet(&version);
-        self.write(&packet)?;
+        self.socket.write_all(&handshake)?;
 
         let ack = self.read_message();
 
@@ -957,7 +1003,7 @@ impl Connection {
             prelude.push_field(&"");
         }
 
-        self.write_message(prelude)?;
+        self.write_message(&prelude)?;
 
         Ok(())
     }
@@ -1045,6 +1091,7 @@ fn parse_connection_time(connection_time: &str) -> (Option<OffsetDateTime>, Opti
     let parts: Vec<&str> = connection_time.split(' ').collect();
 
     let zones = timezones::find_by_name(parts[2]);
+
     if zones.is_empty() {
         error!("time zone not found for {}", parts[2]);
         return (None, None);
@@ -1070,16 +1117,5 @@ fn parse_connection_time(connection_time: &str) -> (Option<OffsetDateTime>, Opti
     }
 }
 
-fn encode_packet(message: &str) -> String {
-    let data = message.as_bytes();
-
-    let mut packet: Vec<u8> = Vec::with_capacity(data.len() + 4);
-
-    packet.write_u32::<BigEndian>(data.len() as u32).unwrap();
-    packet.write_all(data).unwrap();
-
-    std::str::from_utf8(&packet).unwrap().into()
-}
-
 #[cfg(test)]
 mod tests;
diff --git a/src/transport/connection.rs b/src/transport/connection.rs
new file mode 100644
index 00000000..87c27719
--- /dev/null
+++ b/src/transport/connection.rs
@@ -0,0 +1,2 @@
+#[cfg(test)]
+mod tests;
diff --git a/src/transport/connection/tests.rs b/src/transport/connection/tests.rs
new file mode 100644
index 00000000..96edba85
--- /dev/null
+++ b/src/transport/connection/tests.rs
@@ -0,0 +1,461 @@
+use crate::client::Client;
+use crate::contracts::encoders::encode_request_contract_data;
+use crate::contracts::Contract;
+use crate::errors::Error;
+use crate::messages::{encode_length, RequestMessage, ResponseMessage};
+use crate::orders::{Action, order_builder};
+use crate::orders::encoders::encode_place_order;
+use crate::transport::{read_message, Connection, Io, MessageBus, Reconnect, Stream, TcpMessageBus, MAX_RETRIES};
+use std::io::ErrorKind;
+use std::sync::atomic::{AtomicUsize, Ordering};
+
+use log::{debug, trace};
+use std::collections::VecDeque;
+use std::sync::Arc;
+
+fn mock_socket_error(kind: ErrorKind) -> Error {
+    let message = format!("Simulated {} error", kind);
+    debug!("mock -> {message}");
+    let io_error = std::io::Error::new(kind, message);
+    Error::Io(Arc::new(io_error))
+}
+
+#[derive(Debug)]
+struct MockSocket {
+    // Read only
+    exchanges: Vec<Exchange>,
+    expected_retries: usize,
+    reconnect_call_count: AtomicUsize,
+
+    // Accessed from reader thread
+    // Mutated by writer threads
+    exchange_index: AtomicUsize,
+
+    // Mutated from reader thread
+    read_call_count: AtomicUsize,
+
+    // Mutated by writer threads
+    write_call_count: AtomicUsize,
+}
+
+impl MockSocket {
+    pub fn new(exchanges: Vec<Exchange>, expected_retries: usize) -> Self {
+        Self {
+            exchanges,
+            expected_retries,
+            reconnect_call_count: AtomicUsize::new(0),
+
+            exchange_index: AtomicUsize::new(0),
+            read_call_count: AtomicUsize::new(0),
+            write_call_count: AtomicUsize::new(0),
+        }
+    }
+}
+
+impl Reconnect for MockSocket {
+    fn reconnect(&self) -> Result<(), Error> {
+        let reconnect_call_count = self.reconnect_call_count.load(Ordering::SeqCst);
+
+        if reconnect_call_count == self.expected_retries {
+            return Ok(());
+        }
+
+        self.reconnect_call_count.fetch_add(1, Ordering::SeqCst);
+        return Err(mock_socket_error(ErrorKind::ConnectionRefused));
+    }
+    fn sleep(&self, duration: std::time::Duration) {}
+}
+
+impl Stream for MockSocket {}
+
+impl Io for MockSocket {
+    fn read_message(&self) -> Result<Vec<u8>, Error> {
+        let exchange_index = self.exchange_index.load(Ordering::SeqCst);
+        let exchange = match self.exchanges.get(exchange_index) {
+            Some(ex) => ex,
+            None => {
+                // keep alive
+                return Err(mock_socket_error(ErrorKind::WouldBlock));
+            }
+        };
+
+        let response_index = self.read_call_count.load(Ordering::SeqCst);
+        let responses = &exchange.responses;
+        let new_response_index = (response_index + 1) % responses.len();
+
+        trace!(
+            "mock read: responses.len(): {}, response_index: {}, new_response_index: {}, exchange_index: {}",
+            responses.len(),
+            self.read_call_count.load(Ordering::SeqCst),
+            new_response_index,
+            self.exchange_index.load(Ordering::SeqCst)
+        );
+
+        let response = responses.get(response_index).unwrap();
+
+        self.read_call_count.store(new_response_index, Ordering::SeqCst);
+
+        // disconnect if a null byte response is encountered
+        if response.fields[0] == "\0" {
+            return Err(mock_socket_error(ErrorKind::ConnectionReset));
+        }
+
+        // process the declared response in the test with transport read_message()
+        // to force any errors
+        let encoded = response.encode();
+        debug!("mock -> {:?}", &encoded);
+        let expected = encode_length(&encoded);
+        Ok(read_message(&mut expected.as_slice())?)
+    }
+
+    fn write_all(&self, buf: &[u8]) -> Result<(), Error> {
+        // do not increment exchange_index on first call
+        // otherwise increment
+        let write_call_count = self.write_call_count.load(Ordering::SeqCst);
+        if write_call_count > 0 {
+            self.exchange_index.fetch_add(1, Ordering::SeqCst);
+        }
+        let exchange_index = self.exchange_index.load(Ordering::SeqCst);
+
+        trace!("mock write: exchange_index: {exchange_index}, write_call_count: {write_call_count}");
+
+        let exchange = self.exchanges.get(exchange_index).unwrap();
+        let request = &exchange.request;
+
+        let is_handshake = buf.starts_with(b"API\0");
+
+        // strip API\0 if handshake
+        let buf = if is_handshake {
+            &buf[4..] // strip prefix
+        } else {
+            &buf
+        };
+
+        // the handshake does not include the trailing null byte
+        // Message encode() cannot be used to encode the handshake
+        let expected = if is_handshake {
+            assert_eq!(request.len(), 1);
+            &encode_length(&request.fields[0])
+        } else {
+            &encode_length(&request.encode())
+        };
+
+        let raw_string = str::from_utf8(&buf[4..]).unwrap(); // strip length
+        debug!("mock -> {:?}", raw_string);
+
+        assert_eq!(
+            expected,
+            buf,
+            "assertion left == right failed\nleft: {:?}\n right: {:?}\n",
+            str::from_utf8(expected).unwrap(),
+            str::from_utf8(buf).unwrap()
+        );
+
+        self.write_call_count.fetch_add(1, Ordering::SeqCst);
+
+        Ok(())
+    }
+}
+
+#[derive(Debug)]
+struct Exchange {
+    request: RequestMessage,
+    responses: VecDeque<ResponseMessage>,
+}
+
+impl Exchange {
+    fn new(request: RequestMessage, responses: Vec<ResponseMessage>) -> Self {
+        Self {
+            request,
+            responses: VecDeque::from(responses),
+        }
+    }
+    fn simple(request: &str, responses: &[&str]) -> Self {
+        let responses = responses
+            .into_iter()
+            .map(|s| ResponseMessage::from_simple(s))
+            .collect::<Vec<ResponseMessage>>();
+        Self::new(RequestMessage::from_simple(request), responses)
+    }
+    fn request(request: RequestMessage, responses: &[&str]) -> Self {
+        let responses = responses
+            .into_iter()
+            .map(|s| ResponseMessage::from_simple(s))
+            .collect::<Vec<ResponseMessage>>();
+        Self::new(request, responses)
+    }
+}
+
+#[test]
+// #[ignore = "TODO"]
+fn test_bus_send_order_request() -> Result<(), Error> {
+    let order = order_builder::market_order(Action::Buy, 100.0);
+    let contract = &Contract::stock("AAPL");
+    let request = encode_place_order(176, 5, contract, &order)?;
+
+    let events = vec![
+        Exchange::simple("v100..173", &["173|20250415 19:38:30 British Summer Time|"]),
+        Exchange::simple("71|2|28|", &["15|1|DU1234567|", "9|1|5|"]),
+        Exchange::request(request.clone(), 
+            &[
+                "5|5|265598|AAPL|STK||0|?||SMART|USD|AAPL|NMS|BUY|100|MKT|0.0|0.0|DAY||DU1234567||0||100|600745656|0|0|0||600745656.0/DU1234567/100||||||||||0||-1|0||||||2147483647|0|0|0||3|0|0||0|0||0|None||0||||?|0|0||0|0||||||0|0|0|2147483647|2147483647|||0||IB|0|0||0|0|PreSubmitted|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308||||||0|0|0|None|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308|0||||0|1|0|0|0|||0||100|0.02|||",
+                "3|5|PreSubmitted|0|100|0|600745656|0|0|100||0|",
+                "11|-1|5|265598|AAPL|STK||0.0|||IEX|USD|AAPL|NMS|0000e0d5.67fe667b.01.01|20250415  19:38:31|DU1234567|IEX|BOT|100|201.94|600745656|100|0|100|201.94|||||2|",
+                "5|5|265598|AAPL|STK||0|?||SMART|USD|AAPL|NMS|BUY|100|MKT|0.0|0.0|DAY||DU1234567||0||100|600745656|0|0|0||600745656.0/DU1234567/100||||||||||0||-1|0||||||2147483647|0|0|0||3|0|0||0|0||0|None||0||||?|0|0||0|0||||||0|0|0|2147483647|2147483647|||0||IB|0|0||0|0|Filled|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308||||||0|0|0|None|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308|1.7976931348623157E308|0||||0|1|0|0|0|||0||100|0.02|||",
+                "3|5|Filled|100|0|201.94|600745656|0|201.94|100||0|"
+            ]),
+    ];
+
+    let stream = MockSocket::new(events, 0);
+    let connection = Connection::connect(stream, 28)?;
+    let server_version = connection.server_version();
+    let bus = Arc::new(TcpMessageBus::new(connection)?);
+
+    let subscription = bus.send_order_request(5, &request)?;
+
+    bus.dispatch(server_version)?;
+    bus.dispatch(server_version)?;
+    bus.dispatch(server_version)?;
+    bus.dispatch(server_version)?;
+    bus.dispatch(server_version)?;
+
+    subscription.next().unwrap()?;
+
+    Ok(())
+}
+
+#[test]
+fn test_connection_establish_connection() -> Result<(), Error> {
+    let events = vec![
+        Exchange::simple("v100..173", &["173|20250323 22:21:01 Greenwich Mean Time|"]),
+        Exchange::simple(
+            "71|2|28|",
+            &[
+                "15|1|DU1234567|",
+                "9|1|1|",
+                "4|2|-1|2104|Market data farm connection is OK:usfarm||",
+                "4|2|-1|2107|HMDS data farm connection is inactive but should be available upon demand.ushmds||",
+                "4|2|-1|2158|Sec-def data farm connection is OK:secdefil||",
+            ],
+        ),
+    ];
+    let stream = MockSocket::new(events, 0);
+    Connection::connect(stream, 28)?;
+
+    Ok(())
+}
+
+#[test]
+fn test_reconnect_failed() -> Result<(), Error> {
+    let events = vec![
+        Exchange::simple("v100..173", &["173|20250323 22:21:01 Greenwich Mean Time|"]),
+        Exchange::simple("71|2|28|", &["15|1|DU1234567|", "9|1|1|", "\0"]), // RESTART
+    ];
+    let socket = MockSocket::new(events, MAX_RETRIES as usize + 1);
+
+    let connection = Connection::connect(socket, 28)?;
+
+    // simulated dispatcher thread read to trigger disconnection
+    connection.read_message();
+
+    match connection.reconnect() {
+        Err(Error::ConnectionFailed) => return Ok(()),
+        _ => panic!(""),
+    }
+}
+
+#[test]
+fn test_reconnect_success() -> Result<(), Error> {
+    let events = vec![
+        Exchange::simple("v100..173", &["173|20250323 22:21:01 Greenwich Mean Time|"]),
+        Exchange::simple("71|2|28|", &["15|1|DU1234567|", "9|1|1|", "\0"]), // RESTART
+        Exchange::simple("v100..173", &["173|20250323 22:21:01 Greenwich Mean Time|"]),
+        Exchange::simple("71|2|28|", &["15|1|DU1234567|", "9|1|1|"]),
+    ];
+    let socket = MockSocket::new(events, MAX_RETRIES as usize - 1);
+
+    let connection = Connection::connect(socket, 28)?;
+
+    // simulated dispatcher thread read to trigger disconnection
+    connection.read_message();
+
+    Ok(connection.reconnect()?)
+}
+
+// TODO: test takes minimum 1 sec due to signal_recv.recv_timeout(Duration::from_secs(1)) in
+// MessageBus::start_cleanup_thread()
+#[test]
+fn test_client_reconnect() -> Result<(), Error> {
+    // TODO: why 17|1 and not 17|1| for a shared request to assert true in MockSocket write_all ??
+    let events = vec![
+        Exchange::simple("v100..173", &["173|20250323 22:21:01 Greenwich Mean Time|"]),
+        Exchange::simple("71|2|28|", &["15|1|DU1234567|", "9|1|1|"]),
+        Exchange::simple("17|1", &["\0"]), // ManagedAccounts RESTART
+        Exchange::simple("v100..173", &["173|20250323 22:21:01 Greenwich Mean Time|"]),
+        Exchange::simple("71|2|28|", &["15|1|DU1234567|", "9|1|1|"]),
+        Exchange::simple("17|1", &["15|1|DU1234567|"]), // ManagedAccounts
+    ];
+    let stream = MockSocket::new(events, 0);
+    let connection = Connection::connect(stream, 28)?;
+    let server_version = connection.server_version();
+    let bus = Arc::new(TcpMessageBus::new(connection)?);
+    bus.process_messages(server_version)?;
+    let client = Client::stubbed(bus.clone(), server_version);
+
+    client.managed_accounts()?;
+
+    Ok(())
+}
+
+const AAPL_CONTRACT_RESPONSE: &str  = "AAPL|STK||0||SMART|USD|AAPL|NMS|NMS|265598|0.01||ACTIVETIM,AD,ADDONT,ADJUST,ALERT,ALGO,ALLOC,AON,AVGCOST,BASKET,BENCHPX,CASHQTY,COND,CONDORDER,DARKONLY,DARKPOLL,DAY,DEACT,DEACTDIS,DEACTEOD,DIS,DUR,GAT,GTC,GTD,GTT,HID,IBKRATS,ICE,IMB,IOC,LIT,LMT,LOC,MIDPX,MIT,MKT,MOC,MTL,NGCOMB,NODARK,NONALGO,OCA,OPG,OPGREROUT,PEGBENCH,PEGMID,POSTATS,POSTONLY,PREOPGRTH,PRICECHK,REL,REL2MID,RELPCTOFS,RPI,RTH,SCALE,SCALEODD,SCALERST,SIZECHK,SMARTSTG,SNAPMID,SNAPMKT,SNAPREL,STP,STPLMT,SWEEP,TRAIL,TRAILLIT,TRAILLMT,TRAILMIT,WHATIF|SMART,AMEX,NYSE,CBOE,PHLX,ISE,CHX,ARCA,NASDAQ,DRCTEDGE,BEX,BATS,EDGEA,BYX,IEX,EDGX,FOXRIVER,PEARL,NYSENAT,LTSE,MEMX,IBEOS,OVERNIGHT,TPLUS0,PSX|1|0|APPLE INC|NASDAQ||Technology|Computers|Computers|US/Eastern|20250324:0400-20250324:2000;20250325:0400-20250325:2000;20250326:0400-20250326:2000;20250327:0400-20250327:2000;20250328:0400-20250328:2000|20250324:0930-20250324:1600;20250325:0930-20250325:1600;20250326:0930-20250326:1600;20250327:0930-20250327:1600;20250328:0930-20250328:1600|||1|ISIN|US0378331005|1|||26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26||COMMON|0.0001|0.0001|100|";
+
+#[test]
+fn test_send_request_after_disconnect() -> Result<(), Error> {
+    let packet = encode_request_contract_data(173, 9000, &Contract::stock("AAPL"))?;
+
+    let expected_response = &format!("10|9000|{}", AAPL_CONTRACT_RESPONSE);
+
+    let events = vec![
+        Exchange::simple("v100..173", &["173|20250323 22:21:01 Greenwich Mean Time|"]),
+        Exchange::simple("71|2|28|", &["15|1|DU1234567|", "9|1|1|", "\0"]), // RESTART
+        Exchange::simple("v100..173", &["173|20250323 22:21:01 Greenwich Mean Time|"]),
+        Exchange::simple("71|2|28|", &["15|1|DU1234567|", "9|1|1|"]),
+        Exchange::request(packet.clone(), &[expected_response, "52|1|9001|"]),
+    ];
+
+    let stream = MockSocket::new(events, 0);
+    let connection = Connection::connect(stream, 28)?;
+    let server_version = connection.server_version();
+    let bus = TcpMessageBus::new(connection)?;
+
+    bus.dispatch(server_version)?;
+
+    let subscription = bus.send_request(9000, &packet)?;
+
+    bus.dispatch(server_version)?;
+    bus.dispatch(server_version)?;
+
+    subscription.next().unwrap()?;
+
+    // TODO: assert after encoding is fixed
+    // assert_eq!(&result.encode_simple(), expected_response);
+
+    Ok(())
+}
+
+//
+// // Test Error::ConnectionReset is raised on subscription.next()
+// // when sending request during disconnect
+#[test]
+fn test_request_before_disconnect_raises_error() -> Result<(), Error> {
+    let packet = encode_request_contract_data(173, 9000, &Contract::stock("AAPL"))?;
+
+    let events = vec![
+        Exchange::simple("v100..173", &["173|20250323 22:21:01 Greenwich Mean Time|"]),
+        Exchange::simple("71|2|28|", &["15|1|DU1234567|", "9|1|1|"]),
+        Exchange::request(packet.clone(), &["\0"]), // RESTART
+        Exchange::simple("v100..173", &["173|20250323 22:21:01 Greenwich Mean Time|"]),
+        Exchange::simple("71|2|28|", &["15|1|DU1234567|", "9|1|1|"]),
+    ];
+
+    let stream = MockSocket::new(events, 0);
+    let connection = Connection::connect(stream, 28)?;
+    let server_version = connection.server_version();
+    let bus = TcpMessageBus::new(connection)?;
+
+    let subscription = bus.send_request(9000, &packet)?;
+
+    bus.dispatch(server_version)?;
+
+    match subscription.next() {
+        Some(Err(Error::ConnectionReset)) => {}
+        _ => panic!(),
+    }
+
+    Ok(())
+}
+//
+// // Test Error::ConnectionReset is raised on subscription.next()
+// // when sending request during disconnect
+#[test]
+fn test_request_during_disconnect_raises_error() -> Result<(), Error> {
+    env_logger::init();
+    let packet = encode_request_contract_data(173, 9000, &Contract::stock("AAPL"))?;
+
+    let events = vec![
+        Exchange::simple("v100..173", &["173|20250323 22:21:01 Greenwich Mean Time|"]),
+        Exchange::simple("71|2|28|", &["15|1|DU1234567|", "9|1|1|", "\0"]), // RESTART
+        Exchange::simple("v100..173", &["173|20250323 22:21:01 Greenwich Mean Time|"]),
+        Exchange::request(packet.clone(), &[]),
+        Exchange::simple("71|2|28|", &["15|1|DU1234567|", "9|1|1|"]),
+    ];
+
+    let stream = MockSocket::new(events, 0);
+    let connection = Connection::connect(stream, 28)?;
+
+    match connection.read_message() {
+        Ok(_) => panic!(""),
+        Err(_) => {
+            debug!("reconnect");
+            connection.socket.reconnect()?;
+            debug!("handshake");
+            connection.handshake()?;
+            debug!("write_message");
+            connection.write_message(&packet)?;
+            debug!("start_api");
+            connection.start_api()?;
+            connection.receive_account_info()?;
+        }
+    };
+
+    Ok(())
+}
+//
+//
+// // TODO: This test repeats test_request_during_disconnect() with the client instead
+// // the response should be the same, Error::ConnectionReset
+// #[test]
+// #[ignore = "propagate error from contract_details() to fix"]
+// fn test_client_request_during_disconnect() -> Result<(), Box<dyn std::error::Error>> {
+//     let events = vec![
+//         Event::handshake("v100..173", &["173|20250323 22:21:01 Greenwich Mean Time|"]),
+//         Event::request("71|2|28|", &["15|1|DU1234567|", "9|1|1|"]),
+//         Event::Restart,
+//         Event::handshake("v100..173", &["173|20250323 22:21:01 Greenwich Mean Time|"]),
+//         Event::request("71|2|28|", &["15|1|DU1234567|", "9|1|1|"]),
+//     ];
+//
+//     let server = TestServer::start(events);
+//
+//     let client = Client::connect(&server.address().to_string(), 28).unwrap();
+//
+//     // sleep so the request is sent after the dispatcher thread enters the reconnection
+//     // routine
+//     std::thread::sleep(Duration::from_millis(1));
+//
+//     // now attempt to send the request
+//     let contract = &Contract::stock("AAPL");
+//
+//     match client.contract_details(&contract) {
+//         Err(Error::ConnectionReset) => {}
+//         _ => panic!(),
+//     }
+//
+//     Ok(())
+// }
+//
+// // TODO: fix this
+// #[test]
+// #[ignore = ""]
+// fn test_simple_encoding_roundtrip() {
+//     // let res = RequestMessage::from_simple(&format!("10|9000|{}", AAPL_CONTRACT_RESPONSE));
+//     let expected = "17|1|";
+//
+//     let req = RequestMessage::from_simple(expected);
+//     let req = RequestMessage::from(&req.encode()).encode_simple();
+//     assert_eq!(req, expected);
+//
+//     let res = RequestMessage::from_simple(expected);
+//     let res = RequestMessage::from(&res.encode()).encode_simple();
+//     assert_eq!(res, expected);
+// }
diff --git a/src/transport/tests.rs b/src/transport/tests.rs
index a798e154..fd5b59dd 100644
--- a/src/transport/tests.rs
+++ b/src/transport/tests.rs
@@ -1,3 +1,4 @@
+use crate::transport::TcpSocket;
 use time::macros::datetime;
 use time_tz::{timezones, OffsetResult, PrimitiveDateTimeExt};
 
@@ -7,8 +8,8 @@ use super::*;
 
 #[test]
 fn test_thread_safe() {
-    assert_send_and_sync::<Connection>();
-    assert_send_and_sync::<TcpMessageBus>();
+    assert_send_and_sync::<Connection<TcpSocket>>();
+    assert_send_and_sync::<TcpMessageBus<TcpSocket>>();
 }
 
 #[test]


Reviews:
user: login: wboayue
body: Good eye. This was a gap I had intended to close at some point.

Mocking the server is also, in my opinion, the best approach to increase coverage for the `MessageBus`. There are libraries like `mockito` to help generate mock servers, but it's geared towards a specific use cases. I also think the custom mock you’re suggesting will result in code that’s easier to follow.

Overall, I think this is the right approach. We just need to refine the code a bit. I like the use of enumerated `Message` types for the mock messages. I prefer using structs for things like `MockServer` so there’s a clean place to attach configuration and cleanup logic. We'll also need to ensure that the tests can still run in parallel which  looks to be handled by the use of dynamically allocated ports.
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-25T23:03:13Z
user: login: fhill2
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T20:11:26Z
user: login: fhill2
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T20:19:56Z
user: login: fhill2
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T20:28:40Z
user: login: wboayue
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-02T23:04:23Z
user: login: wboayue
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-02T23:11:50Z
user: login: wboayue
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-02T23:35:34Z
user: login: wboayue
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-02T23:43:37Z
user: login: wboayue
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-02T23:46:28Z
user: login: fhill2
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T15:54:41Z
user: login: fhill2
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T15:57:32Z
user: login: wboayue
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-15T23:00:38Z
user: login: wboayue
body: This looks great, thanks @fhill2. 🚀  

I like the additional traits, they improve the modularity and testability of the MessageBus. I think we have enough to merge this PR. We can incrementally add the tests marked TODO.
state: APPROVED
author_association: OWNER
submitted_at: 2025-04-15T23:16:18Z
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.


<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/messages.rs:367**
* The 'fields' member in RequestMessage is now public; please confirm that exposing the internal structure is intended for testing or broader use. If encapsulation is desired, consider maintaining it as private with appropriate accessor methods.
```
pub(crate) fields: Vec<String>,
```
**src/transport.rs:238**
* [nitpick] Consider adding an inline comment to clarify the reconnect logic in this branch, as the handling of connection failures is critical for maintainability.
```
if let Err(reconnect_err) = self.connection.reconnect() {
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-04-16T00:41:30Z

Review comments:
diff_hunk: @@ -349,6 +352,50 @@ impl ToField for OutgoingMessages {
     }
 }
 
+fn encode_packet(message: &str) -> String {
+    let data = message.as_bytes();
+
+    let mut packet: Vec<u8> = Vec::with_capacity(data.len() + 4);
+
+    packet.write_u32::<BigEndian>(data.len() as u32).unwrap();
+    packet.write_all(data).unwrap();
+
+    std::str::from_utf8(&packet).unwrap().into()
+}
+
+#[derive(Default, Debug, Clone)]
+pub(crate) struct RequestHandshake {
+    min_version: i32,
+    max_version: i32,
+}
+impl RequestHandshake {
path: src/messages.rs
user: login: fhill2
body: I've created a different request type for the RequestHandshake so we can create handshake bytestrings to test with more easily
created_at: 2025-04-01T20:11:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,272 @@
+use crate::client::Client;
+use crate::contracts::Contract;
+use crate::messages::{RequestHandshake, RequestMessage, ResponseMessage};
+use crate::transport::read_header;
+use log::debug;
+use std::collections::VecDeque;
+use std::io::{Read, Write};
+use std::net::TcpStream;
+use std::net::{SocketAddr, TcpListener};
+use std::str::from_utf8;
+use std::thread::JoinHandle;
+
+// Build Events with Stubs
+// struct EventBuilder {
+//     events:
+// }
+// impl EventBuilder {
+//     startup()
+// }
+
+// Trying to keep the shared functionality between the TestServer and Connection
+// in a location they can be accessed by both
+
+// #[derive(Clone, Debug)]
+// struct Message {
+//     request: RequestMessage,
+//     responses: Vec<ResponseMessage>,
+// }
+#[derive(Clone, Debug)]
+enum Message {
+    Handshake {
+        request: RequestHandshake,
+        responses: Vec<ResponseMessage>,
+    },
+    Request {
+        request: RequestMessage,
+        responses: Vec<ResponseMessage>,
+    },
+    // Subscription
+}
+
+#[derive(Clone, Debug)]
+enum Event {
+    Restart,
+    Message(Message),
+}
+
+impl Event {
+    fn request(request: &str, responses: &[&str]) -> Self {
+        let responses = responses
+            .into_iter()
+            .map(|s| ResponseMessage::from_simple(s))
+            .collect::<Vec<ResponseMessage>>();
+        let message = Message::Request {
+            request: RequestMessage::from_simple(request),
+            responses,
+        };
+        Event::Message(message)
+    }
+
+    fn handshake(request: &str, responses: &[&str]) -> Self {
+        let responses = responses
+            .into_iter()
+            .map(|s| ResponseMessage::from_simple(s))
+            .collect::<Vec<ResponseMessage>>();
+        let message = Message::Handshake {
+            request: RequestHandshake::from_simple(request),
+            responses,
+        };
+        Event::Message(message)
+    }
+}
+
+struct MockStream {
+    stream: TcpStream,
+    messages: VecDeque<Message>,
+    // keep the stream alive after messages have processed
+    keep_alive: bool,
+}
+
+impl MockStream {
+    pub fn new(stream: TcpStream, messages: VecDeque<Message>, keep_alive: bool) -> Self {
+        Self {
+            stream,
+            messages,
+            keep_alive,
+        }
+    }
+
+    fn read_prefix(&mut self) {
+        let mut data = vec![0_u8; 4];
+        self.stream.read(&mut data).unwrap();
+        assert_eq!(
+            data,
+            b"API\0",
+            "Handshake message did not start with API prefix.\n  left: {}\n  right: {}",
+            from_utf8(&data).unwrap(),
+            from_utf8(&b"API\0"[..]).unwrap(),
+        );
+    }
+
+    fn handle_message(&mut self, request: String, responses: Vec<ResponseMessage>) {
+        let message_size = read_header(&self.stream).unwrap();
+
+        let mut data = vec![0_u8; message_size];
+
+        self.stream.read_exact(&mut data).unwrap();
+
+        let raw_string = String::from_utf8(data).unwrap();
+        debug!("mock <- {:#?}", raw_string);
+
+        // let request = message.request.encode();
+
+        assert_eq!(
path: src/transport/connection/tests.rs
user: login: fhill2
body: The TestServer needs to generate the length encoded bytestring for each event of the test to compare with the incoming client requests. 
created_at: 2025-04-01T20:19:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -349,6 +352,50 @@ impl ToField for OutgoingMessages {
     }
 }
 
+fn encode_packet(message: &str) -> String {
+    let data = message.as_bytes();
+
+    let mut packet: Vec<u8> = Vec::with_capacity(data.len() + 4);
+
+    packet.write_u32::<BigEndian>(data.len() as u32).unwrap();
+    packet.write_all(data).unwrap();
+
+    std::str::from_utf8(&packet).unwrap().into()
+}
+
+#[derive(Default, Debug, Clone)]
+pub(crate) struct RequestHandshake {
+    min_version: i32,
+    max_version: i32,
+}
+impl RequestHandshake {
+    pub fn new(min_version: i32, max_version: i32) -> Self {
+        Self { min_version, max_version }
+    }
+    pub fn encode(&self) -> String {
+        format!("v{}..{}", self.min_version, self.max_version)
+    }
+
+    pub fn packet(&self) -> String {
path: src/messages.rs
user: login: fhill2
body: I've moved the length encoding from the Connection to the Message as there is `encode()` there already, and the method to produce the final bytes relies on the type of Message.
created_at: 2025-04-01T20:28:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,272 @@
+use crate::client::Client;
+use crate::contracts::Contract;
+use crate::messages::{RequestHandshake, RequestMessage, ResponseMessage};
+use crate::transport::read_header;
+use log::debug;
+use std::collections::VecDeque;
+use std::io::{Read, Write};
+use std::net::TcpStream;
+use std::net::{SocketAddr, TcpListener};
+use std::str::from_utf8;
+use std::thread::JoinHandle;
+
+// Build Events with Stubs
+// struct EventBuilder {
+//     events:
+// }
+// impl EventBuilder {
+//     startup()
+// }
+
+// Trying to keep the shared functionality between the TestServer and Connection
+// in a location they can be accessed by both
+
+// #[derive(Clone, Debug)]
+// struct Message {
+//     request: RequestMessage,
+//     responses: Vec<ResponseMessage>,
+// }
+#[derive(Clone, Debug)]
+enum Message {
+    Handshake {
+        request: RequestHandshake,
+        responses: Vec<ResponseMessage>,
+    },
+    Request {
+        request: RequestMessage,
+        responses: Vec<ResponseMessage>,
+    },
+    // Subscription
+}
+
+#[derive(Clone, Debug)]
+enum Event {
+    Restart,
+    Message(Message),
+}
+
+impl Event {
+    fn request(request: &str, responses: &[&str]) -> Self {
+        let responses = responses
+            .into_iter()
+            .map(|s| ResponseMessage::from_simple(s))
+            .collect::<Vec<ResponseMessage>>();
+        let message = Message::Request {
+            request: RequestMessage::from_simple(request),
+            responses,
+        };
+        Event::Message(message)
+    }
+
+    fn handshake(request: &str, responses: &[&str]) -> Self {
+        let responses = responses
+            .into_iter()
+            .map(|s| ResponseMessage::from_simple(s))
+            .collect::<Vec<ResponseMessage>>();
+        let message = Message::Handshake {
+            request: RequestHandshake::from_simple(request),
+            responses,
+        };
+        Event::Message(message)
+    }
+}
+
+struct MockStream {
+    stream: TcpStream,
+    messages: VecDeque<Message>,
+    // keep the stream alive after messages have processed
+    keep_alive: bool,
+}
+
+impl MockStream {
+    pub fn new(stream: TcpStream, messages: VecDeque<Message>, keep_alive: bool) -> Self {
+        Self {
+            stream,
+            messages,
+            keep_alive,
+        }
+    }
+
+    fn read_prefix(&mut self) {
+        let mut data = vec![0_u8; 4];
+        self.stream.read(&mut data).unwrap();
+        assert_eq!(
+            data,
+            b"API\0",
+            "Handshake message did not start with API prefix.\n  left: {}\n  right: {}",
+            from_utf8(&data).unwrap(),
+            from_utf8(&b"API\0"[..]).unwrap(),
+        );
+    }
+
+    fn handle_message(&mut self, request: String, responses: Vec<ResponseMessage>) {
+        let message_size = read_header(&self.stream).unwrap();
+
+        let mut data = vec![0_u8; message_size];
+
+        self.stream.read_exact(&mut data).unwrap();
+
+        let raw_string = String::from_utf8(data).unwrap();
+        debug!("mock <- {:#?}", raw_string);
+
+        // let request = message.request.encode();
+
+        assert_eq!(
+            raw_string,
+            request,
+            // "Mock Server event request != Client request.\n  left: {},\n  right: {}",
+        );
+
+        for res in responses {
+            let packet = res.packet();
+            debug!("mock -> {:#?}", packet);
+            self.stream.write_all(packet.as_bytes()).unwrap();
+        }
+    }
+
+    pub fn process(&mut self) {
+        loop {
+            let message = match self.messages.pop_front() {
+                Some(message) => message,
+                None => break,
+            };
+
+            match message {
+                Message::Handshake { request, responses } => {
+                    self.read_prefix();
+                    self.handle_message(request.encode(), responses);
+                }
+                Message::Request { request, responses } => self.handle_message(request.encode(), responses),
+            }
+        }
+
+        if self.keep_alive {
+            self.keep_alive()
+        }
+    }
+
+    fn keep_alive(&mut self) {
+        let mut data = vec![0_u8; 1024];
+        loop {
+            match self.stream.read(&mut data) {
+                Ok(0) => {
+                    debug!("Client closed connection to Mock Server");
+                    break;
+                }
+                Ok(_) => {
+                    panic!("Mock Server received more client requests than declared");
+                }
+                Err(e) => {
+                    panic!("Mock Server stream errored during keep_alive: {}", e);
+                }
+            };
+        }
+    }
+}
+
+struct MockServer {
+    listener: TcpListener,
+}
+
+impl MockServer {
+    fn new(listener: TcpListener) -> Self {
+        Self { listener }
+    }
+
+    fn process(&self, mut events: VecDeque<Event>) {
+        loop {
+            if events.is_empty() {
+                break;
+            }
+
+            let mut messages = VecDeque::new();
+            while let Some(event) = events.pop_front() {
+                match event {
+                    Event::Message(message) => messages.push_back(message),
+                    Event::Restart => break,
+                }
+            }
+
+            MockStream::new(self.listener.accept().unwrap().0, messages, events.is_empty()).process();
+        }
+    }
+}
+
+struct TestServer {
+    listener: TcpListener,
+    handle: JoinHandle<()>,
+}
+impl TestServer {
+    fn start(events: Vec<Event>) -> Self {
+        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
+
+        let listener_clone = listener.try_clone().unwrap();
+
+        let handle = std::thread::spawn(move || {
+            let server = MockServer::new(listener_clone);
+            server.process(VecDeque::from(events));
+        });
+        Self { listener, handle }
+    }
+    fn address(&self) -> SocketAddr {
+        self.listener.local_addr().unwrap()
+    }
+}
+
+const AAPL_CONTRACT: &str  = "AAPL|STK||0||SMART|USD|AAPL|NMS|NMS|265598|0.01||ACTIVETIM,AD,ADDONT,ADJUST,ALERT,ALGO,ALLOC,AON,AVGCOST,BASKET,BENCHPX,CASHQTY,COND,CONDORDER,DARKONLY,DARKPOLL,DAY,DEACT,DEACTDIS,DEACTEOD,DIS,DUR,GAT,GTC,GTD,GTT,HID,IBKRATS,ICE,IMB,IOC,LIT,LMT,LOC,MIDPX,MIT,MKT,MOC,MTL,NGCOMB,NODARK,NONALGO,OCA,OPG,OPGREROUT,PEGBENCH,PEGMID,POSTATS,POSTONLY,PREOPGRTH,PRICECHK,REL,REL2MID,RELPCTOFS,RPI,RTH,SCALE,SCALEODD,SCALERST,SIZECHK,SMARTSTG,SNAPMID,SNAPMKT,SNAPREL,STP,STPLMT,SWEEP,TRAIL,TRAILLIT,TRAILLMT,TRAILMIT,WHATIF|SMART,AMEX,NYSE,CBOE,PHLX,ISE,CHX,ARCA,NASDAQ,DRCTEDGE,BEX,BATS,EDGEA,BYX,IEX,EDGX,FOXRIVER,PEARL,NYSENAT,LTSE,MEMX,IBEOS,OVERNIGHT,TPLUS0,PSX|1|0|APPLE INC|NASDAQ||Technology|Computers|Computers|US/Eastern|20250324:0400-20250324:2000;20250325:0400-20250325:2000;20250326:0400-20250326:2000;20250327:0400-20250327:2000;20250328:0400-20250328:2000|20250324:0930-20250324:1600;20250325:0930-20250325:1600;20250326:0930-20250326:1600;20250327:0930-20250327:1600;20250328:0930-20250328:1600|||1|ISIN|US0378331005|1|||26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26||COMMON|0.0001|0.0001|100|";
+
+// Examples:
+// Testing connect -> request -> disconnect -> reconnect -> request
+#[test]
+fn test_mock_connection() {
path: src/transport/connection/tests.rs
user: login: wboayue
body: I think this will be easier to reason about if broken into 3 tests.

* **test_establish_connection** - would test just handshake and start api
* **test_send_request** - test a single request (e.g. connect and get  contract details)
* **test_reconnect** - test ability to reconnect. simplest way to do this is with an API like `Client::managed_accounts`. I haven't gone back and added this to every API that could support this but some like `Client::managed_accounts` can automatically retry after a reconnect.

created_at: 2025-04-02T23:04:22Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,272 @@
+use crate::client::Client;
+use crate::contracts::Contract;
+use crate::messages::{RequestHandshake, RequestMessage, ResponseMessage};
+use crate::transport::read_header;
+use log::debug;
+use std::collections::VecDeque;
+use std::io::{Read, Write};
+use std::net::TcpStream;
+use std::net::{SocketAddr, TcpListener};
+use std::str::from_utf8;
+use std::thread::JoinHandle;
+
+// Build Events with Stubs
+// struct EventBuilder {
+//     events:
+// }
+// impl EventBuilder {
+//     startup()
+// }
+
+// Trying to keep the shared functionality between the TestServer and Connection
+// in a location they can be accessed by both
+
+// #[derive(Clone, Debug)]
+// struct Message {
+//     request: RequestMessage,
+//     responses: Vec<ResponseMessage>,
+// }
+#[derive(Clone, Debug)]
+enum Message {
+    Handshake {
+        request: RequestHandshake,
+        responses: Vec<ResponseMessage>,
+    },
+    Request {
+        request: RequestMessage,
+        responses: Vec<ResponseMessage>,
+    },
+    // Subscription
+}
+
+#[derive(Clone, Debug)]
+enum Event {
+    Restart,
+    Message(Message),
+}
+
+impl Event {
+    fn request(request: &str, responses: &[&str]) -> Self {
+        let responses = responses
+            .into_iter()
+            .map(|s| ResponseMessage::from_simple(s))
+            .collect::<Vec<ResponseMessage>>();
+        let message = Message::Request {
+            request: RequestMessage::from_simple(request),
+            responses,
+        };
+        Event::Message(message)
+    }
+
+    fn handshake(request: &str, responses: &[&str]) -> Self {
+        let responses = responses
+            .into_iter()
+            .map(|s| ResponseMessage::from_simple(s))
+            .collect::<Vec<ResponseMessage>>();
+        let message = Message::Handshake {
+            request: RequestHandshake::from_simple(request),
+            responses,
+        };
+        Event::Message(message)
+    }
+}
+
+struct MockStream {
+    stream: TcpStream,
+    messages: VecDeque<Message>,
+    // keep the stream alive after messages have processed
+    keep_alive: bool,
+}
+
+impl MockStream {
+    pub fn new(stream: TcpStream, messages: VecDeque<Message>, keep_alive: bool) -> Self {
+        Self {
+            stream,
+            messages,
+            keep_alive,
+        }
+    }
+
+    fn read_prefix(&mut self) {
+        let mut data = vec![0_u8; 4];
+        self.stream.read(&mut data).unwrap();
+        assert_eq!(
+            data,
+            b"API\0",
+            "Handshake message did not start with API prefix.\n  left: {}\n  right: {}",
+            from_utf8(&data).unwrap(),
+            from_utf8(&b"API\0"[..]).unwrap(),
+        );
+    }
+
+    fn handle_message(&mut self, request: String, responses: Vec<ResponseMessage>) {
+        let message_size = read_header(&self.stream).unwrap();
+
+        let mut data = vec![0_u8; message_size];
+
+        self.stream.read_exact(&mut data).unwrap();
+
+        let raw_string = String::from_utf8(data).unwrap();
+        debug!("mock <- {:#?}", raw_string);
+
+        // let request = message.request.encode();
+
+        assert_eq!(
+            raw_string,
+            request,
+            // "Mock Server event request != Client request.\n  left: {},\n  right: {}",
+        );
+
+        for res in responses {
+            let packet = res.packet();
+            debug!("mock -> {:#?}", packet);
+            self.stream.write_all(packet.as_bytes()).unwrap();
+        }
+    }
+
+    pub fn process(&mut self) {
+        loop {
+            let message = match self.messages.pop_front() {
+                Some(message) => message,
+                None => break,
+            };
+
+            match message {
+                Message::Handshake { request, responses } => {
+                    self.read_prefix();
+                    self.handle_message(request.encode(), responses);
+                }
+                Message::Request { request, responses } => self.handle_message(request.encode(), responses),
+            }
+        }
+
+        if self.keep_alive {
+            self.keep_alive()
+        }
+    }
+
+    fn keep_alive(&mut self) {
+        let mut data = vec![0_u8; 1024];
+        loop {
+            match self.stream.read(&mut data) {
+                Ok(0) => {
+                    debug!("Client closed connection to Mock Server");
+                    break;
+                }
+                Ok(_) => {
+                    panic!("Mock Server received more client requests than declared");
+                }
+                Err(e) => {
+                    panic!("Mock Server stream errored during keep_alive: {}", e);
+                }
+            };
+        }
+    }
+}
+
+struct MockServer {
+    listener: TcpListener,
+}
+
+impl MockServer {
+    fn new(listener: TcpListener) -> Self {
+        Self { listener }
+    }
+
+    fn process(&self, mut events: VecDeque<Event>) {
+        loop {
+            if events.is_empty() {
+                break;
+            }
+
+            let mut messages = VecDeque::new();
+            while let Some(event) = events.pop_front() {
+                match event {
+                    Event::Message(message) => messages.push_back(message),
+                    Event::Restart => break,
+                }
+            }
+
+            MockStream::new(self.listener.accept().unwrap().0, messages, events.is_empty()).process();
+        }
+    }
+}
+
+struct TestServer {
+    listener: TcpListener,
+    handle: JoinHandle<()>,
+}
+impl TestServer {
+    fn start(events: Vec<Event>) -> Self {
+        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
+
+        let listener_clone = listener.try_clone().unwrap();
+
+        let handle = std::thread::spawn(move || {
+            let server = MockServer::new(listener_clone);
+            server.process(VecDeque::from(events));
+        });
+        Self { listener, handle }
+    }
+    fn address(&self) -> SocketAddr {
+        self.listener.local_addr().unwrap()
+    }
+}
+
+const AAPL_CONTRACT: &str  = "AAPL|STK||0||SMART|USD|AAPL|NMS|NMS|265598|0.01||ACTIVETIM,AD,ADDONT,ADJUST,ALERT,ALGO,ALLOC,AON,AVGCOST,BASKET,BENCHPX,CASHQTY,COND,CONDORDER,DARKONLY,DARKPOLL,DAY,DEACT,DEACTDIS,DEACTEOD,DIS,DUR,GAT,GTC,GTD,GTT,HID,IBKRATS,ICE,IMB,IOC,LIT,LMT,LOC,MIDPX,MIT,MKT,MOC,MTL,NGCOMB,NODARK,NONALGO,OCA,OPG,OPGREROUT,PEGBENCH,PEGMID,POSTATS,POSTONLY,PREOPGRTH,PRICECHK,REL,REL2MID,RELPCTOFS,RPI,RTH,SCALE,SCALEODD,SCALERST,SIZECHK,SMARTSTG,SNAPMID,SNAPMKT,SNAPREL,STP,STPLMT,SWEEP,TRAIL,TRAILLIT,TRAILLMT,TRAILMIT,WHATIF|SMART,AMEX,NYSE,CBOE,PHLX,ISE,CHX,ARCA,NASDAQ,DRCTEDGE,BEX,BATS,EDGEA,BYX,IEX,EDGX,FOXRIVER,PEARL,NYSENAT,LTSE,MEMX,IBEOS,OVERNIGHT,TPLUS0,PSX|1|0|APPLE INC|NASDAQ||Technology|Computers|Computers|US/Eastern|20250324:0400-20250324:2000;20250325:0400-20250325:2000;20250326:0400-20250326:2000;20250327:0400-20250327:2000;20250328:0400-20250328:2000|20250324:0930-20250324:1600;20250325:0930-20250325:1600;20250326:0930-20250326:1600;20250327:0930-20250327:1600;20250328:0930-20250328:1600|||1|ISIN|US0378331005|1|||26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26||COMMON|0.0001|0.0001|100|";
+
+// Examples:
+// Testing connect -> request -> disconnect -> reconnect -> request
+#[test]
+fn test_mock_connection() {
+    env_logger::init();
+
+    // these messages could also be read from the output of MessageRecorder...
+    // assuming the MessageRecorder records the full establish_connection() routine
+    let events = vec![
+        Event::handshake("v100..173", &["173|20250323 22:21:01 Greenwich Mean Time|"]),
+        Event::request("71|2|28|", &["15|1|DU1234567|", "9|1|1|"]),
+        Event::request(
+            "9|8|9000|0|AAPL|STK||0|||SMART||USD|||0||",
+            &[&format!("10|9000|{}", AAPL_CONTRACT), "52|1|9000|"],
+        ),
+        Event::Restart,
+        Event::handshake("v100..173", &["173|20250323 22:21:01 Greenwich Mean Time|"]),
+        Event::request("71|2|28|", &["15|1|DU1234567|", "9|1|1|"]),
+        // Event::request(
+        //     "9|8|9001|0|AAPL|STK||0|||SMART||USD|||0||",
+        //     &[&format!("10|9001|{}", AAPL_CONTRACT), "52|1|9001|"],
+        // ),
+    ];
+
+    let server = TestServer::start(events);
+
+    let client = Client::connect(&server.address().to_string(), 28).unwrap();
+
+    let contract = Contract::stock("AAPL");
+    client.contract_details(&contract).unwrap();
+
+    // TODO: this should wait until the client has reconnected instead of sleeping
path: src/transport/connection/tests.rs
user: login: wboayue
body: If we test with one of the auto reconnect methods, we won't need a sleep.
created_at: 2025-04-02T23:11:50Z
author_association: OWNER
diff_hunk: @@ -349,6 +352,50 @@ impl ToField for OutgoingMessages {
     }
 }
 
+fn encode_packet(message: &str) -> String {
+    let data = message.as_bytes();
+
+    let mut packet: Vec<u8> = Vec::with_capacity(data.len() + 4);
+
+    packet.write_u32::<BigEndian>(data.len() as u32).unwrap();
+    packet.write_all(data).unwrap();
+
+    std::str::from_utf8(&packet).unwrap().into()
+}
+
+#[derive(Default, Debug, Clone)]
+pub(crate) struct RequestHandshake {
+    min_version: i32,
+    max_version: i32,
+}
+impl RequestHandshake {
+    pub fn new(min_version: i32, max_version: i32) -> Self {
+        Self { min_version, max_version }
+    }
+    pub fn encode(&self) -> String {
+        format!("v{}..{}", self.min_version, self.max_version)
+    }
+
+    pub fn packet(&self) -> String {
+        // prefix does not get length encoded
+        "API\0".to_owned() + &encode_packet(&self.encode())
path: src/messages.rs
user: login: wboayue
body: I also conflated these in my implementation, but taking a second look I think these are separate things.
Logically, I think it's better to think of `API\0` as the handshake  and `v100..173` as the first message.
This will simplify the test setup to just one message type.
created_at: 2025-04-02T23:35:34Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,272 @@
+use crate::client::Client;
+use crate::contracts::Contract;
+use crate::messages::{RequestHandshake, RequestMessage, ResponseMessage};
+use crate::transport::read_header;
+use log::debug;
+use std::collections::VecDeque;
+use std::io::{Read, Write};
+use std::net::TcpStream;
+use std::net::{SocketAddr, TcpListener};
+use std::str::from_utf8;
+use std::thread::JoinHandle;
+
+// Build Events with Stubs
+// struct EventBuilder {
+//     events:
+// }
+// impl EventBuilder {
+//     startup()
+// }
+
+// Trying to keep the shared functionality between the TestServer and Connection
+// in a location they can be accessed by both
+
+// #[derive(Clone, Debug)]
+// struct Message {
+//     request: RequestMessage,
+//     responses: Vec<ResponseMessage>,
+// }
+#[derive(Clone, Debug)]
+enum Message {
path: src/transport/connection/tests.rs
user: login: wboayue
body: If we treat the handshake as just `API\0` can replace this with a single struct, I like  `MockExchange`, that contains the request/replies pair.
created_at: 2025-04-02T23:43:37Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,272 @@
+use crate::client::Client;
+use crate::contracts::Contract;
+use crate::messages::{RequestHandshake, RequestMessage, ResponseMessage};
+use crate::transport::read_header;
+use log::debug;
+use std::collections::VecDeque;
+use std::io::{Read, Write};
+use std::net::TcpStream;
+use std::net::{SocketAddr, TcpListener};
+use std::str::from_utf8;
+use std::thread::JoinHandle;
+
+// Build Events with Stubs
+// struct EventBuilder {
+//     events:
+// }
+// impl EventBuilder {
+//     startup()
+// }
+
+// Trying to keep the shared functionality between the TestServer and Connection
+// in a location they can be accessed by both
+
+// #[derive(Clone, Debug)]
+// struct Message {
+//     request: RequestMessage,
+//     responses: Vec<ResponseMessage>,
+// }
+#[derive(Clone, Debug)]
+enum Message {
+    Handshake {
+        request: RequestHandshake,
+        responses: Vec<ResponseMessage>,
+    },
+    Request {
+        request: RequestMessage,
+        responses: Vec<ResponseMessage>,
+    },
+    // Subscription
+}
+
+#[derive(Clone, Debug)]
+enum Event {
+    Restart,
+    Message(Message),
+}
+
+impl Event {
+    fn request(request: &str, responses: &[&str]) -> Self {
+        let responses = responses
+            .into_iter()
+            .map(|s| ResponseMessage::from_simple(s))
+            .collect::<Vec<ResponseMessage>>();
+        let message = Message::Request {
+            request: RequestMessage::from_simple(request),
+            responses,
+        };
+        Event::Message(message)
+    }
+
+    fn handshake(request: &str, responses: &[&str]) -> Self {
+        let responses = responses
+            .into_iter()
+            .map(|s| ResponseMessage::from_simple(s))
+            .collect::<Vec<ResponseMessage>>();
+        let message = Message::Handshake {
+            request: RequestHandshake::from_simple(request),
+            responses,
+        };
+        Event::Message(message)
+    }
+}
+
+struct MockStream {
+    stream: TcpStream,
+    messages: VecDeque<Message>,
+    // keep the stream alive after messages have processed
+    keep_alive: bool,
+}
+
+impl MockStream {
+    pub fn new(stream: TcpStream, messages: VecDeque<Message>, keep_alive: bool) -> Self {
+        Self {
+            stream,
+            messages,
+            keep_alive,
+        }
+    }
+
+    fn read_prefix(&mut self) {
+        let mut data = vec![0_u8; 4];
+        self.stream.read(&mut data).unwrap();
+        assert_eq!(
+            data,
+            b"API\0",
+            "Handshake message did not start with API prefix.\n  left: {}\n  right: {}",
+            from_utf8(&data).unwrap(),
+            from_utf8(&b"API\0"[..]).unwrap(),
+        );
+    }
+
+    fn handle_message(&mut self, request: String, responses: Vec<ResponseMessage>) {
+        let message_size = read_header(&self.stream).unwrap();
+
+        let mut data = vec![0_u8; message_size];
+
+        self.stream.read_exact(&mut data).unwrap();
+
+        let raw_string = String::from_utf8(data).unwrap();
+        debug!("mock <- {:#?}", raw_string);
+
+        // let request = message.request.encode();
+
+        assert_eq!(
+            raw_string,
+            request,
+            // "Mock Server event request != Client request.\n  left: {},\n  right: {}",
+        );
+
+        for res in responses {
+            let packet = res.packet();
+            debug!("mock -> {:#?}", packet);
+            self.stream.write_all(packet.as_bytes()).unwrap();
+        }
+    }
+
+    pub fn process(&mut self) {
+        loop {
+            let message = match self.messages.pop_front() {
+                Some(message) => message,
+                None => break,
+            };
+
+            match message {
+                Message::Handshake { request, responses } => {
+                    self.read_prefix();
path: src/transport/connection/tests.rs
user: login: wboayue
body: Instead of a special message type, read_prefix will always be the first thing that the server does.
created_at: 2025-04-02T23:46:28Z
author_association: OWNER
diff_hunk: @@ -349,6 +352,50 @@ impl ToField for OutgoingMessages {
     }
 }
 
+fn encode_packet(message: &str) -> String {
+    let data = message.as_bytes();
+
+    let mut packet: Vec<u8> = Vec::with_capacity(data.len() + 4);
+
+    packet.write_u32::<BigEndian>(data.len() as u32).unwrap();
+    packet.write_all(data).unwrap();
+
+    std::str::from_utf8(&packet).unwrap().into()
+}
+
+#[derive(Default, Debug, Clone)]
+pub(crate) struct RequestHandshake {
+    min_version: i32,
+    max_version: i32,
+}
+impl RequestHandshake {
+    pub fn new(min_version: i32, max_version: i32) -> Self {
+        Self { min_version, max_version }
+    }
+    pub fn encode(&self) -> String {
+        format!("v{}..{}", self.min_version, self.max_version)
+    }
+
+    pub fn packet(&self) -> String {
+        // prefix does not get length encoded
+        "API\0".to_owned() + &encode_packet(&self.encode())
path: src/messages.rs
user: login: fhill2
body: The handshake also requires the absence of the trailing null byte. This means we can't use [RequestMethod::encode()](https://github.com/wboayue/rust-ibapi/blob/8f2ab14beee94172e81404ae8cb040afbbac2923/src/messages.rs#L370) method on the handshake.

```txt
// correct handshake
b'API\x00\x00\x00\x00\tv100..157'
   
//  RequestMethod { fields: vec!["v176..180"] }.encode()
// raises "Message header length invalid" in TWS / Gateway
b'API\x00\x00\x00\x00\tv100..157\0'
```

So the MockServer would have to:
- have different handling functions for the Message and Handshake (we extend read_prefix()) - both have different reading, encoding handling.
Or
- create a separate struct for the handshake and have a single handling function in the MockServer (we could also simplify the handling in the Connection too)
created_at: 2025-04-04T15:54:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,272 @@
+use crate::client::Client;
+use crate::contracts::Contract;
+use crate::messages::{RequestHandshake, RequestMessage, ResponseMessage};
+use crate::transport::read_header;
+use log::debug;
+use std::collections::VecDeque;
+use std::io::{Read, Write};
+use std::net::TcpStream;
+use std::net::{SocketAddr, TcpListener};
+use std::str::from_utf8;
+use std::thread::JoinHandle;
+
+// Build Events with Stubs
+// struct EventBuilder {
+//     events:
+// }
+// impl EventBuilder {
+//     startup()
+// }
+
+// Trying to keep the shared functionality between the TestServer and Connection
+// in a location they can be accessed by both
+
+// #[derive(Clone, Debug)]
+// struct Message {
+//     request: RequestMessage,
+//     responses: Vec<ResponseMessage>,
+// }
+#[derive(Clone, Debug)]
+enum Message {
+    Handshake {
+        request: RequestHandshake,
+        responses: Vec<ResponseMessage>,
+    },
+    Request {
+        request: RequestMessage,
+        responses: Vec<ResponseMessage>,
+    },
+    // Subscription
+}
+
+#[derive(Clone, Debug)]
+enum Event {
+    Restart,
+    Message(Message),
+}
+
+impl Event {
+    fn request(request: &str, responses: &[&str]) -> Self {
+        let responses = responses
+            .into_iter()
+            .map(|s| ResponseMessage::from_simple(s))
+            .collect::<Vec<ResponseMessage>>();
+        let message = Message::Request {
+            request: RequestMessage::from_simple(request),
+            responses,
+        };
+        Event::Message(message)
+    }
+
+    fn handshake(request: &str, responses: &[&str]) -> Self {
+        let responses = responses
+            .into_iter()
+            .map(|s| ResponseMessage::from_simple(s))
+            .collect::<Vec<ResponseMessage>>();
+        let message = Message::Handshake {
+            request: RequestHandshake::from_simple(request),
+            responses,
+        };
+        Event::Message(message)
+    }
+}
+
+struct MockStream {
+    stream: TcpStream,
+    messages: VecDeque<Message>,
+    // keep the stream alive after messages have processed
+    keep_alive: bool,
+}
+
+impl MockStream {
+    pub fn new(stream: TcpStream, messages: VecDeque<Message>, keep_alive: bool) -> Self {
+        Self {
+            stream,
+            messages,
+            keep_alive,
+        }
+    }
+
+    fn read_prefix(&mut self) {
+        let mut data = vec![0_u8; 4];
+        self.stream.read(&mut data).unwrap();
+        assert_eq!(
+            data,
+            b"API\0",
+            "Handshake message did not start with API prefix.\n  left: {}\n  right: {}",
+            from_utf8(&data).unwrap(),
+            from_utf8(&b"API\0"[..]).unwrap(),
+        );
+    }
+
+    fn handle_message(&mut self, request: String, responses: Vec<ResponseMessage>) {
+        let message_size = read_header(&self.stream).unwrap();
+
+        let mut data = vec![0_u8; message_size];
+
+        self.stream.read_exact(&mut data).unwrap();
+
+        let raw_string = String::from_utf8(data).unwrap();
+        debug!("mock <- {:#?}", raw_string);
+
+        // let request = message.request.encode();
+
+        assert_eq!(
+            raw_string,
+            request,
+            // "Mock Server event request != Client request.\n  left: {},\n  right: {}",
+        );
+
+        for res in responses {
+            let packet = res.packet();
+            debug!("mock -> {:#?}", packet);
+            self.stream.write_all(packet.as_bytes()).unwrap();
+        }
+    }
+
+    pub fn process(&mut self) {
+        loop {
+            let message = match self.messages.pop_front() {
+                Some(message) => message,
+                None => break,
+            };
+
+            match message {
+                Message::Handshake { request, responses } => {
+                    self.read_prefix();
+                    self.handle_message(request.encode(), responses);
+                }
+                Message::Request { request, responses } => self.handle_message(request.encode(), responses),
+            }
+        }
+
+        if self.keep_alive {
+            self.keep_alive()
+        }
+    }
+
+    fn keep_alive(&mut self) {
+        let mut data = vec![0_u8; 1024];
+        loop {
+            match self.stream.read(&mut data) {
+                Ok(0) => {
+                    debug!("Client closed connection to Mock Server");
+                    break;
+                }
+                Ok(_) => {
+                    panic!("Mock Server received more client requests than declared");
+                }
+                Err(e) => {
+                    panic!("Mock Server stream errored during keep_alive: {}", e);
+                }
+            };
+        }
+    }
+}
+
+struct MockServer {
+    listener: TcpListener,
+}
+
+impl MockServer {
+    fn new(listener: TcpListener) -> Self {
+        Self { listener }
+    }
+
+    fn process(&self, mut events: VecDeque<Event>) {
+        loop {
+            if events.is_empty() {
+                break;
+            }
+
+            let mut messages = VecDeque::new();
+            while let Some(event) = events.pop_front() {
+                match event {
+                    Event::Message(message) => messages.push_back(message),
+                    Event::Restart => break,
+                }
+            }
+
+            MockStream::new(self.listener.accept().unwrap().0, messages, events.is_empty()).process();
+        }
+    }
+}
+
+struct TestServer {
+    listener: TcpListener,
+    handle: JoinHandle<()>,
+}
+impl TestServer {
+    fn start(events: Vec<Event>) -> Self {
+        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
+
+        let listener_clone = listener.try_clone().unwrap();
+
+        let handle = std::thread::spawn(move || {
+            let server = MockServer::new(listener_clone);
+            server.process(VecDeque::from(events));
+        });
+        Self { listener, handle }
+    }
+    fn address(&self) -> SocketAddr {
+        self.listener.local_addr().unwrap()
+    }
+}
+
+const AAPL_CONTRACT: &str  = "AAPL|STK||0||SMART|USD|AAPL|NMS|NMS|265598|0.01||ACTIVETIM,AD,ADDONT,ADJUST,ALERT,ALGO,ALLOC,AON,AVGCOST,BASKET,BENCHPX,CASHQTY,COND,CONDORDER,DARKONLY,DARKPOLL,DAY,DEACT,DEACTDIS,DEACTEOD,DIS,DUR,GAT,GTC,GTD,GTT,HID,IBKRATS,ICE,IMB,IOC,LIT,LMT,LOC,MIDPX,MIT,MKT,MOC,MTL,NGCOMB,NODARK,NONALGO,OCA,OPG,OPGREROUT,PEGBENCH,PEGMID,POSTATS,POSTONLY,PREOPGRTH,PRICECHK,REL,REL2MID,RELPCTOFS,RPI,RTH,SCALE,SCALEODD,SCALERST,SIZECHK,SMARTSTG,SNAPMID,SNAPMKT,SNAPREL,STP,STPLMT,SWEEP,TRAIL,TRAILLIT,TRAILLMT,TRAILMIT,WHATIF|SMART,AMEX,NYSE,CBOE,PHLX,ISE,CHX,ARCA,NASDAQ,DRCTEDGE,BEX,BATS,EDGEA,BYX,IEX,EDGX,FOXRIVER,PEARL,NYSENAT,LTSE,MEMX,IBEOS,OVERNIGHT,TPLUS0,PSX|1|0|APPLE INC|NASDAQ||Technology|Computers|Computers|US/Eastern|20250324:0400-20250324:2000;20250325:0400-20250325:2000;20250326:0400-20250326:2000;20250327:0400-20250327:2000;20250328:0400-20250328:2000|20250324:0930-20250324:1600;20250325:0930-20250325:1600;20250326:0930-20250326:1600;20250327:0930-20250327:1600;20250328:0930-20250328:1600|||1|ISIN|US0378331005|1|||26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26||COMMON|0.0001|0.0001|100|";
+
+// Examples:
+// Testing connect -> request -> disconnect -> reconnect -> request
+#[test]
+fn test_mock_connection() {
path: src/transport/connection/tests.rs
user: login: fhill2
body: Good idea, I'll get working on these tests + some additional test cases that we can decide to keep or not.
created_at: 2025-04-04T15:57:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -871,56 +936,37 @@ impl Connection {
         Ok(())
     }
 
-    fn write(&self, data: &str) -> Result<(), Error> {
-        let mut writer = self.writer.lock()?;
-        writer.write_all(data.as_bytes())?;
-        Ok(())
-    }
-
     fn write_message(&self, message: &RequestMessage) -> Result<(), Error> {
-        let mut writer = self.writer.lock()?;
-
-        let data = message.encode();
-        debug!("-> {data:?}");
-
-        let data = data.as_bytes();
-
-        let mut packet = Vec::with_capacity(data.len() + 4);
-
-        packet.write_u32::<BigEndian>(data.len() as u32)?;
-        packet.write_all(data)?;
-
-        writer.write_all(&packet)?;
-
         self.recorder.record_request(message);
-
+        let encoded = message.encode();
+        debug!("-> {encoded:?}");
+        let length_encoded = encode_length(&encoded);
path: src/transport.rs
user: login: wboayue
body: Nit: `encoded_with_length` might work better here. There is some ambiguity now between this being the length of the encoded message or the message encoded with length.
created_at: 2025-04-15T23:00:38Z
author_association: OWNER

PR comments:
user: login: fhill2
created_at: 2025-03-26T21:07:57Z
body: That's great, I'll refine this and implement the following over the next week:
- use structs for `MockServer`
- add some additional tests using the mock server.
- remove the temporary sleep() from the tests. 
- teardown the mock server correctly on test end.

author_association: CONTRIBUTOR
user: login: fhill2
created_at: 2025-04-01T21:40:45Z
body: @wboayue how should we handle waiting in the tests for the client to reconnect? Currently I can't find a reliable way to do this.

I've tried waiting based on if a client request method (e.g contract_details()) returns `Err::ConnectionReset` - however I don't think this error is being returned to the test correctly.

Based on the [MessageBus reset() method](https://github.com/wboayue/rust-ibapi/blob/8f2ab14beee94172e81404ae8cb040afbbac2923/src/transport.rs#L197) - the bus should notify the waiters with an Error::ConnectionReset after reconnection.

Below are logs from a test using the mock server, the actions are:
- connect
- contract_details() Send Receive
- disconnect
- contract_details() Send
- reconnect Success <-- should notify all request waiters with Error::ConnectionReset 
- contract_details() Receive <-- empty vec is returned here instead (atleast this is the case for contract_details()).  it should return Error::ConnectionReset


<details>
  <summary>Mock Server Test Logs</summary>

```txt
  [2025-04-01T21:53:42Z DEBUG ibapi::transport] -> "API\0\0\0\0\tv100..173"
[2025-04-01T21:53:42Z DEBUG ibapi::transport::connection::tests] mock <- "v100..173"
[2025-04-01T21:53:42Z DEBUG ibapi::transport::connection::tests] mock -> "\0\0\0+173\020250323 22:21:01 Greenwich Mean Time\0\0"
[2025-04-01T21:53:42Z DEBUG ibapi::transport] <- "173\020250323 22:21:01 Greenwich Mean Time\0\0"
[2025-04-01T21:53:42Z DEBUG ibapi::transport] -> "\0\0\0\t71\02\028\0\0"
[2025-04-01T21:53:42Z DEBUG ibapi::transport::connection::tests] mock <- "71\02\028\0\0"
[2025-04-01T21:53:42Z DEBUG ibapi::transport::connection::tests] mock -> "\0\0\0\u{10}15\01\0DU1234567\0\0"
[2025-04-01T21:53:42Z DEBUG ibapi::transport::connection::tests] mock -> "\0\0\0\u{7}9\01\01\0\0"
[2025-04-01T21:53:42Z DEBUG ibapi::transport] <- "15\01\0DU1234567\0\0"
[2025-04-01T21:53:42Z DEBUG ibapi::transport] <- "9\01\01\0\0"
[2025-04-01T21:53:42Z DEBUG ibapi::client] send_message(9000, RequestMessage { fields: ["9", "8", "9000", "0", "AAPL", "STK", "", "0", "", "", "SMART", "", "USD", "", "", "0", "", ""] })
[2025-04-01T21:53:42Z DEBUG ibapi::transport] -> "\0\0\0*9\08\09000\00\0AAPL\0STK\0\00\0\0\0SMART\0\0USD\0\0\00\0\0\0"
[2025-04-01T21:53:42Z DEBUG ibapi::transport::connection::tests] mock <- "9\08\09000\00\0AAPL\0STK\0\00\0\0\0SMART\0\0USD\0\0\00\0\0\0"
[2025-04-01T21:53:42Z DEBUG ibapi::transport::connection::tests] mock -> "\0\0\u{4}r10\09000\0AAPL\0STK\0\00\0\0SMART\0USD\0AAPL\0NMS\0NMS\0265598\00.01\0\0ACTIVETIM,AD,ADDONT,ADJUST,ALERT,ALGO,ALLOC,AON,AVGCOST,BASKET,BENCHPX,CASHQTY,COND,CONDORDER,DARKONLY,DARKPOLL,DAY,DEACT,DEACTDIS,DEACTEOD,DIS,DUR,GAT,GTC,GTD,GTT,HID,IBKRATS,ICE,IMB,IOC,LIT,LMT,LOC,MIDPX,MIT,MKT,MOC,MTL,NGCOMB,NODARK,NONALGO,OCA,OPG,OPGREROUT,PEGBENCH,PEGMID,POSTATS,POSTONLY,PREOPGRTH,PRICECHK,REL,REL2MID,RELPCTOFS,RPI,RTH,SCALE,SCALEODD,SCALERST,SIZECHK,SMARTSTG,SNAPMID,SNAPMKT,SNAPREL,STP,STPLMT,SWEEP,TRAIL,TRAILLIT,TRAILLMT,TRAILMIT,WHATIF\0SMART,AMEX,NYSE,CBOE,PHLX,ISE,CHX,ARCA,NASDAQ,DRCTEDGE,BEX,BATS,EDGEA,BYX,IEX,EDGX,FOXRIVER,PEARL,NYSENAT,LTSE,MEMX,IBEOS,OVERNIGHT,TPLUS0,PSX\01\00\0APPLE INC\0NASDAQ\0\0Technology\0Computers\0Computers\0US/Eastern\020250324:0400-20250324:2000;20250325:0400-20250325:2000;20250326:0400-20250326:2000;20250327:0400-20250327:2000;20250328:0400-20250328:2000\020250324:0930-20250324:1600;20250325:0930-20250325:1600;20250326:0930-20250326:1600;20250327:0930-20250327:1600;20250328:0930-20250328:1600\0\0\01\0ISIN\0US0378331005\01\0\0\026,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26\0\0COMMON\00.0001\00.0001\0100\0\0"
[2025-04-01T21:53:42Z DEBUG ibapi::transport::connection::tests] mock -> "\0\0\0\u{b}52\01\09000\0\0"
[2025-04-01T21:53:42Z DEBUG ibapi::transport] <- "10\09000\0AAPL\0STK\0\00\0\0SMART\0USD\0AAPL\0NMS\0NMS\0265598\00.01\0\0ACTIVETIM,AD,ADDONT,ADJUST,ALERT,ALGO,ALLOC,AON,AVGCOST,BASKET,BENCHPX,CASHQTY,COND,CONDORDER,DARKONLY,DARKPOLL,DAY,DEACT,DEACTDIS,DEACTEOD,DIS,DUR,GAT,GTC,GTD,GTT,HID,IBKRATS,ICE,IMB,IOC,LIT,LMT,LOC,MIDPX,MIT,MKT,MOC,MTL,NGCOMB,NODARK,NONALGO,OCA,OPG,OPGREROUT,PEGBENCH,PEGMID,POSTATS,POSTONLY,PREOPGRTH,PRICECHK,REL,REL2MID,RELPCTOFS,RPI,RTH,SCALE,SCALEODD,SCALERST,SIZECHK,SMARTSTG,SNAPMID,SNAPMKT,SNAPREL,STP,STPLMT,SWEEP,TRAIL,TRAILLIT,TRAILLMT,TRAILMIT,WHATIF\0SMART,AMEX,NYSE,CBOE,PHLX,ISE,CHX,ARCA,NASDAQ,DRCTEDGE,BEX,BATS,EDGEA,BYX,IEX,EDGX,FOXRIVER,PEARL,NYSENAT,LTSE,MEMX,IBEOS,OVERNIGHT,TPLUS0,PSX\01\00\0APPLE INC\0NASDAQ\0\0Technology\0Computers\0Computers\0US/Eastern\020250324:0400-20250324:2000;20250325:0400-20250325:2000;20250326:0400-20250326:2000;20250327:0400-20250327:2000;20250328:0400-20250328:2000\020250324:0930-20250324:1600;20250325:0930-20250325:1600;20250326:0930-20250326:1600;20250327:0930-20250327:1600;20250328:0930-20250328:1600\0\0\01\0ISIN\0US0378331005\01\0\0\026,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26\0\0COMMON\00.0001\00.0001\0100\0\0"
[2025-04-01T21:53:42Z DEBUG ibapi::transport] senders: {9000: Sender { .. }}
[2025-04-01T21:53:42Z DEBUG ibapi::transport] <- "52\01\09000\0\0"
[2025-04-01T21:53:42Z DEBUG ibapi::transport] senders: {9000: Sender { .. }}
[2025-04-01T21:53:42Z ERROR ibapi::transport] error reading next message (will attempt reconnect): Error { kind: UnexpectedEof, message: "failed to fill whole buffer" }
[2025-04-01T21:53:42Z INFO  ibapi::transport] next reconnection attempt in 1s
details
[ContractDetails { contract: Contract { contract_id: 265598, symbol: "AAPL", security_type: Stock, last_trade_date_or_contract_month: "", strike: 0.0, right: "", multiplier: "", exchange: "SMART", currency: "USD", local_symbol: "AAPL", primary_exchange: "NASDAQ", trading_class: "NMS", include_expired: false, security_id_type: "", security_id: "", combo_legs_description: "", combo_legs: [], delta_neutral_contract: None, issuer_id: "", description: "" }, market_name: "NMS", min_tick: 0.01, price_magnifier: 1, order_types: ["ACTIVETIM", "AD", "ADDONT", "ADJUST", "ALERT", "ALGO", "ALLOC", "AON", "AVGCOST", "BASKET", "BENCHPX", "CASHQTY", "COND", "CONDORDER", "DARKONLY", "DARKPOLL", "DAY", "DEACT", "DEACTDIS", "DEACTEOD", "DIS", "DUR", "GAT", "GTC", "GTD", "GTT", "HID", "IBKRATS", "ICE", "IMB", "IOC", "LIT", "LMT", "LOC", "MIDPX", "MIT", "MKT", "MOC", "MTL", "NGCOMB", "NODARK", "NONALGO", "OCA", "OPG", "OPGREROUT", "PEGBENCH", "PEGMID", "POSTATS", "POSTONLY", "PREOPGRTH", "PRICECHK", "REL", "REL2MID", "RELPCTOFS", "RPI", "RTH", "SCALE", "SCALEODD", "SCALERST", "SIZECHK", "SMARTSTG", "SNAPMID", "SNAPMKT", "SNAPREL", "STP", "STPLMT", "SWEEP", "TRAIL", "TRAILLIT", "TRAILLMT", "TRAILMIT", "WHATIF"], valid_exchanges: ["SMART", "AMEX", "NYSE", "CBOE", "PHLX", "ISE", "CHX", "ARCA", "NASDAQ", "DRCTEDGE", "BEX", "BATS", "EDGEA", "BYX", "IEX", "EDGX", "FOXRIVER", "PEARL", "NYSENAT", "LTSE", "MEMX", "IBEOS", "OVERNIGHT", "TPLUS0", "PSX"], under_contract_id: 0, long_name: "APPLE INC", contract_month: "", industry: "Technology", category: "Computers", subcategory: "Computers", time_zone_id: "US/Eastern", trading_hours: ["20250324:0400-20250324:2000", "20250325:0400-20250325:2000", "20250326:0400-20250326:2000", "20250327:0400-20250327:2000", "20250328:0400-20250328:2000"], liquid_hours: ["20250324:0930-20250324:1600", "20250325:0930-20250325:1600", "20250326:0930-20250326:1600", "20250327:0930-20250327:1600", "20250328:0930-20250328:1600"], ev_rule: "", ev_multiplier: 0.0, agg_group: 1, sec_id_list: [TagValue { tag: "ISIN", value: "US0378331005" }], under_symbol: "", under_security_type: "", market_rule_ids: ["26", "26", "26", "26", "26", "26", "26", "26", "26", "26", "26", "26", "26", "26", "26", "26", "26", "26", "26", "26", "26", "26", "26", "26", "26"], real_expiration_date: "", last_trade_time: "", stock_type: "COMMON", cusip: "", ratings: "", desc_append: "", bond_type: "", coupon_type: "", callable: false, putable: false, coupon: 0.0, convertible: false, maturity: "", issue_date: "", next_option_date: "", next_option_type: "", next_option_partial: false, notes: "", min_size: 0.0001, size_increment: 0.0001, suggested_size_increment: 100.0 }]
[2025-04-01T21:53:42Z DEBUG ibapi::transport] released request_id 9000, requests.len()=0
[2025-04-01T21:53:42Z DEBUG ibapi::client] send_message(9001, RequestMessage { fields: ["9", "8", "9001", "0", "AAPL", "STK", "", "0", "", "", "SMART", "", "USD", "", "", "0", "", ""] })
[2025-04-01T21:53:42Z DEBUG ibapi::transport] -> "\0\0\0*9\08\09001\00\0AAPL\0STK\0\00\0\0\0SMART\0\0USD\0\0\00\0\0\0"
[2025-04-01T21:53:43Z INFO  ibapi::transport] reconnected !!!
[2025-04-01T21:53:43Z DEBUG ibapi::transport] -> "API\0\0\0\0\tv100..173"
[2025-04-01T21:53:43Z DEBUG ibapi::transport::connection::tests] mock <- "v100..173"
[2025-04-01T21:53:43Z DEBUG ibapi::transport::connection::tests] mock -> "\0\0\0+173\020250323 22:21:01 Greenwich Mean Time\0\0"
[2025-04-01T21:53:43Z DEBUG ibapi::transport] <- "173\020250323 22:21:01 Greenwich Mean Time\0\0"
[2025-04-01T21:53:43Z DEBUG ibapi::transport] -> "\0\0\0\t71\02\028\0\0"
[2025-04-01T21:53:43Z DEBUG ibapi::transport::connection::tests] mock <- "71\02\028\0\0"
[2025-04-01T21:53:43Z DEBUG ibapi::transport::connection::tests] mock -> "\0\0\0\u{10}15\01\0DU1234567\0\0"
[2025-04-01T21:53:43Z DEBUG ibapi::transport::connection::tests] mock -> "\0\0\0\u{7}9\01\01\0\0"
[2025-04-01T21:53:43Z DEBUG ibapi::transport] <- "15\01\0DU1234567\0\0"
[2025-04-01T21:53:43Z DEBUG ibapi::transport] <- "9\01\01\0\0"
[2025-04-01T21:53:43Z INFO  ibapi::transport] successfully reconnected to TWS/Gateway
[2025-04-01T21:53:43Z DEBUG ibapi::transport] reset message bus
details
[]
[2025-04-01T21:53:43Z DEBUG ibapi::client] dropping basic client
[2025-04-01T21:53:43Z DEBUG ibapi::transport] shutdown requested
[2025-04-01T21:53:43Z DEBUG ibapi::transport] released request_id 9001, requests.len()=0
[2025-04-01T21:53:43Z DEBUG ibapi::transport] cleanup thread exiting
[2025-04-01T21:53:44Z DEBUG ibapi::transport] dispatcher thread exiting
[2025-04-01T21:53:44Z DEBUG ibapi::transport::connection::tests] Client closed connection to Mock Server

```
</details>


author_association: CONTRIBUTOR
user: login: wboayue
created_at: 2025-04-02T23:47:32Z
body: @fhill2 Thanks for working on this. It's coming along well.
author_association: OWNER
user: login: fhill2
created_at: 2025-04-07T23:43:50Z
body: After working on the tests and using the Mock types I've noticed areas they can be improved:

[Some tests require accurate scheduling of the writes/reads.](https://github.com/fhill2/rust-ibapi/blob/1f5f31e11b20f50dc5c3b0e5115889c5a13a9c95/src/transport/connection/tests.rs#L347) For example, ensuring waiters return the same response if a request is made before the reconnect, and during the reconnect. 

The tests also depend on external resources. Currently each test requires a socket with a localhost remote.

I've made some changes to the Connection that fixes these 2 problems:

I've moved the contents of TcpMessageBus `start_dispatcher_thread()` into a separate method so it can be called from the main thread. Executing this on the main thread in the test between writes guarantees correct scheduling.

```rust
fn test_connection_no_threads() -> Result<(), Box<dyn std::error::Error>> {
    
    ....

    bus.dispatch(server_version)?;

    let subscription = bus.send_request(9000, &packet)?;

    bus.dispatch(server_version)?;
    bus.dispatch(server_version)?;

    let result = subscription.next().unwrap();

    Ok(())
}
```

Also, the std::net::TcpStream is mocked so there are no external calls. To accomplish this the Connection and TcpMessageBus are generic over trait `Stream`, and a struct that impl `Stream` is passed to the Connection instead. I've named this `TcpSocket` (for lack of a better name), 

`MockSocket` replaces `TcpSocket` in testing and uses a buffer to mock the stream.

`Io` and `Reconnect` are supertraits of `Stream` - However these could be combined into a single trait.

```rust
pub trait Stream: Io + Reconnect + Sync + Send + 'static + std::fmt::Debug {}
pub trait Reconnect {
    fn reconnect(&self) -> Result<(), Error>;
}
pub trait Io {
    fn read_exact(&self, buf: &mut [u8]) -> Result<(), Error>;
    fn write_all(&self, buf: &[u8]) -> Result<(), Error>;
}
```

I've also refined based on your previous comments:
- removed RequestHandshake struct, and changed the Message enum to a Message struct.  
- the mock always checks if the [message contains the prefix](https://github.com/fhill2/rust-ibapi/blob/0a17a5c2612ed046c5d35a7fcc07642c405961b6/src/transport/connection/tests.rs#L35)
- Message struct renamed to MockExchange

Let me know your thoughts on the new changes.

Currently there is a single working test using the new version [here](https://github.com/fhill2/rust-ibapi/blob/0a17a5c2612ed046c5d35a7fcc07642c405961b6/src/transport/connection/tests.rs#L162) that connects -> reconnects -> send_request.
author_association: CONTRIBUTOR