Skip to content

Commit 7598479

Browse files
committed
Refactor session tests into multiple modules
1 parent e1ad8e7 commit 7598479

3 files changed

Lines changed: 130 additions & 129 deletions

File tree

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
use crate::common::mock_application::MockApplication;
2+
use crate::common::mock_counterparty::MockCounterparty;
3+
use crate::common::session_assertions::SessionAssertions;
4+
use crate::common::test_messages::TestMessage;
5+
use hotfix::application::ApplicationRef;
6+
use hotfix::config::SessionConfig;
7+
use hotfix::session::{SessionRef, Status};
8+
use hotfix::store::in_memory::InMemoryMessageStore;
9+
use hotfix_message::Part;
10+
use hotfix_message::fix44::MSG_TYPE;
11+
12+
const HEARTBEAT_INTERVAL: u64 = 30;
13+
14+
/// Tests the automatic heartbeat mechanism in an active FIX session:
15+
/// 1. Establishes a session by exchanging logon messages with the counterparty
16+
/// 2. Advances time beyond the configured heartbeat interval
17+
/// 3. Verifies that a heartbeat message (type '0') is automatically sent
18+
/// 4. Cleanly disconnects the session
19+
///
20+
/// This test ensures that the session maintains connectivity by sending
21+
/// periodic heartbeat messages when no other messages are being exchanged,
22+
/// as required by the FIX protocol to prevent timeout disconnections.
23+
#[tokio::test(start_paused = true)]
24+
async fn test_heartbeats() {
25+
let (session, mut mock_counterparty) = setup().await;
26+
27+
// assert that a logon message is received (type 'A')
28+
mock_counterparty
29+
.assert_next(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "A"))
30+
.await;
31+
// counterparty responds with a logon to establish a happy session
32+
mock_counterparty.send_logon().await;
33+
tokio::task::yield_now().await;
34+
35+
// let's wait enough time for a heartbeat and assert that the heartbeat was sent
36+
tokio::time::advance(std::time::Duration::from_secs(HEARTBEAT_INTERVAL + 1)).await;
37+
mock_counterparty
38+
.assert_next(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "0"))
39+
.await;
40+
41+
session
42+
.disconnect("Test Session Finished".to_string())
43+
.await;
44+
mock_counterparty.assert_disconnected().await;
45+
}
46+
47+
/// Tests the peer timeout and disconnection mechanism:
48+
/// 1. Establishes a session by exchanging logon messages
49+
/// 2. Simulates peer inactivity by advancing time past the peer timeout threshold
50+
/// 3. Verifies that a TestRequest message is sent to check peer responsiveness
51+
/// 4. Continues to simulate peer silence and verifies automatic disconnection
52+
///
53+
/// This test ensures the session properly handles unresponsive peers by first
54+
/// attempting to verify connectivity with a TestRequest, then disconnecting
55+
/// if no response is received within the timeout period.
56+
#[tokio::test(start_paused = true)]
57+
async fn test_peer_timeout() {
58+
let (session, mut mock_counterparty) = setup().await;
59+
let peer_interval = (1.2 * HEARTBEAT_INTERVAL as f64) as u64 + 1;
60+
61+
// assert that a logon message is received (type 'A')
62+
mock_counterparty
63+
.assert_next(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "A"))
64+
.await;
65+
session.assert_status(Status::AwaitingLogon).await;
66+
67+
// counterparty responds with a logon to establish a happy session
68+
mock_counterparty.send_logon().await;
69+
session.assert_status(Status::Active).await;
70+
71+
// let's wait enough time for a heartbeat and assert that the heartbeat was sent
72+
tokio::time::advance(std::time::Duration::from_secs(HEARTBEAT_INTERVAL + 1)).await;
73+
mock_counterparty
74+
.assert_next(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "0"))
75+
.await;
76+
77+
// we wait enough time for the peer deadline to pass
78+
tokio::time::advance(std::time::Duration::from_secs(
79+
peer_interval - HEARTBEAT_INTERVAL,
80+
))
81+
.await;
82+
// a TestRequest (type '1') is sent to the counterparty
83+
mock_counterparty
84+
.assert_next(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "1"))
85+
.await;
86+
87+
// we wait even longer and the counterparty never responds, so we disconnect from the counterparty
88+
tokio::time::advance(std::time::Duration::from_secs(peer_interval)).await;
89+
mock_counterparty.assert_disconnected().await;
90+
}
91+
92+
async fn setup() -> (SessionRef<TestMessage>, MockCounterparty<TestMessage>) {
93+
let config = create_session_config();
94+
let counterparty_config = create_counterparty_session_config(config.clone());
95+
96+
let application_ref = ApplicationRef::new(MockApplication {});
97+
let message_store = InMemoryMessageStore::default();
98+
99+
let session = SessionRef::new(config, application_ref, message_store);
100+
let mock_counterparty = MockCounterparty::start(session.clone(), counterparty_config).await;
101+
102+
(session, mock_counterparty)
103+
}
104+
105+
fn create_session_config() -> SessionConfig {
106+
SessionConfig {
107+
begin_string: "FIX.4.4".to_string(),
108+
sender_comp_id: "dummy-initiator".to_string(),
109+
target_comp_id: "dummy-acceptor".to_string(),
110+
data_dictionary_path: None,
111+
connection_host: "".to_string(),
112+
connection_port: 0,
113+
tls_config: None,
114+
heartbeat_interval: HEARTBEAT_INTERVAL,
115+
reconnect_interval: 30,
116+
reset_on_logon: false,
117+
schedule: None,
118+
}
119+
}
120+
121+
/// Create a session configuration for the counterparty from our configuration.
122+
fn create_counterparty_session_config(session_config: SessionConfig) -> SessionConfig {
123+
SessionConfig {
124+
sender_comp_id: session_config.target_comp_id.clone(),
125+
target_comp_id: session_config.sender_comp_id.clone(),
126+
..session_config
127+
}
128+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mod heartbeat_tests;
Lines changed: 1 addition & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,2 @@
1-
use crate::common::mock_application::MockApplication;
2-
use crate::common::mock_counterparty::MockCounterparty;
3-
use crate::common::session_assertions::SessionAssertions;
4-
use crate::common::test_messages::TestMessage;
5-
use hotfix::application::ApplicationRef;
6-
use hotfix::config::SessionConfig;
7-
use hotfix::session::{SessionRef, Status};
8-
use hotfix::store::in_memory::InMemoryMessageStore;
9-
use hotfix_message::Part;
10-
use hotfix_message::fix44::MSG_TYPE;
11-
121
mod common;
13-
14-
const HEARTBEAT_INTERVAL: u64 = 30;
15-
16-
/// Tests the automatic heartbeat mechanism in an active FIX session:
17-
/// 1. Establishes a session by exchanging logon messages with the counterparty
18-
/// 2. Advances time beyond the configured heartbeat interval
19-
/// 3. Verifies that a heartbeat message (type '0') is automatically sent
20-
/// 4. Cleanly disconnects the session
21-
///
22-
/// This test ensures that the session maintains connectivity by sending
23-
/// periodic heartbeat messages when no other messages are being exchanged,
24-
/// as required by the FIX protocol to prevent timeout disconnections.
25-
#[tokio::test(start_paused = true)]
26-
async fn test_heartbeats() {
27-
let (session, mut mock_counterparty) = setup().await;
28-
29-
// assert that a logon message is received (type 'A')
30-
mock_counterparty
31-
.assert_next(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "A"))
32-
.await;
33-
// counterparty responds with a logon to establish a happy session
34-
mock_counterparty.send_logon().await;
35-
tokio::task::yield_now().await;
36-
37-
// let's wait enough time for a heartbeat and assert that the heartbeat was sent
38-
tokio::time::advance(std::time::Duration::from_secs(HEARTBEAT_INTERVAL + 1)).await;
39-
mock_counterparty
40-
.assert_next(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "0"))
41-
.await;
42-
43-
session
44-
.disconnect("Test Session Finished".to_string())
45-
.await;
46-
mock_counterparty.assert_disconnected().await;
47-
}
48-
49-
/// Tests the peer timeout and disconnection mechanism:
50-
/// 1. Establishes a session by exchanging logon messages
51-
/// 2. Simulates peer inactivity by advancing time past the peer timeout threshold
52-
/// 3. Verifies that a TestRequest message is sent to check peer responsiveness
53-
/// 4. Continues to simulate peer silence and verifies automatic disconnection
54-
///
55-
/// This test ensures the session properly handles unresponsive peers by first
56-
/// attempting to verify connectivity with a TestRequest, then disconnecting
57-
/// if no response is received within the timeout period.
58-
#[tokio::test(start_paused = true)]
59-
async fn test_peer_timeout() {
60-
let (session, mut mock_counterparty) = setup().await;
61-
let peer_interval = (1.2 * HEARTBEAT_INTERVAL as f64) as u64 + 1;
62-
63-
// assert that a logon message is received (type 'A')
64-
mock_counterparty
65-
.assert_next(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "A"))
66-
.await;
67-
session.assert_status(Status::AwaitingLogon).await;
68-
69-
// counterparty responds with a logon to establish a happy session
70-
mock_counterparty.send_logon().await;
71-
session.assert_status(Status::Active).await;
72-
73-
// let's wait enough time for a heartbeat and assert that the heartbeat was sent
74-
tokio::time::advance(std::time::Duration::from_secs(HEARTBEAT_INTERVAL + 1)).await;
75-
mock_counterparty
76-
.assert_next(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "0"))
77-
.await;
78-
79-
// we wait enough time for the peer deadline to pass
80-
tokio::time::advance(std::time::Duration::from_secs(
81-
peer_interval - HEARTBEAT_INTERVAL,
82-
))
83-
.await;
84-
// a TestRequest (type '1') is sent to the counterparty
85-
mock_counterparty
86-
.assert_next(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "1"))
87-
.await;
88-
89-
// we wait even longer and the counterparty never responds, so we disconnect from the counterparty
90-
tokio::time::advance(std::time::Duration::from_secs(peer_interval)).await;
91-
mock_counterparty.assert_disconnected().await;
92-
}
93-
94-
async fn setup() -> (SessionRef<TestMessage>, MockCounterparty<TestMessage>) {
95-
let config = create_session_config();
96-
let counterparty_config = create_counterparty_session_config(config.clone());
97-
98-
let application_ref = ApplicationRef::new(MockApplication {});
99-
let message_store = InMemoryMessageStore::default();
100-
101-
let session = SessionRef::new(config, application_ref, message_store);
102-
let mock_counterparty = MockCounterparty::start(session.clone(), counterparty_config).await;
103-
104-
(session, mock_counterparty)
105-
}
106-
107-
fn create_session_config() -> SessionConfig {
108-
SessionConfig {
109-
begin_string: "FIX.4.4".to_string(),
110-
sender_comp_id: "dummy-initiator".to_string(),
111-
target_comp_id: "dummy-acceptor".to_string(),
112-
data_dictionary_path: None,
113-
connection_host: "".to_string(),
114-
connection_port: 0,
115-
tls_config: None,
116-
heartbeat_interval: HEARTBEAT_INTERVAL,
117-
reconnect_interval: 30,
118-
reset_on_logon: false,
119-
schedule: None,
120-
}
121-
}
122-
123-
/// Create a session configuration for the counterparty from our configuration.
124-
fn create_counterparty_session_config(session_config: SessionConfig) -> SessionConfig {
125-
SessionConfig {
126-
sender_comp_id: session_config.target_comp_id.clone(),
127-
target_comp_id: session_config.sender_comp_id.clone(),
128-
..session_config
129-
}
130-
}
2+
mod session_test_cases;

0 commit comments

Comments
 (0)