Skip to content

Commit 17d2fb4

Browse files
committed
Disconnect counterparty if they respond with non-logon message to logon
1 parent 36f5554 commit 17d2fb4

3 files changed

Lines changed: 112 additions & 6 deletions

File tree

crates/hotfix/src/session.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ impl<M: FixMessage, S: MessageStore> Session<M, S> {
198198
}
199199

200200
async fn process_message(&mut self, message: Message) -> Result<()> {
201+
let message_type = message.header().get(fix44::MSG_TYPE)?;
202+
201203
if let SessionState::AwaitingResend(state) = &mut self.state {
202204
// TODO: consider what messages won't have a sequence number?
203205
// e.g. SequenceReset?
@@ -210,9 +212,17 @@ impl<M: FixMessage, S: MessageStore> Session<M, S> {
210212
return Ok(());
211213
}
212214
}
215+
216+
if let SessionState::AwaitingLogon { .. } = &mut self.state {
217+
// TODO: should this (and all inbound message processing) logic be pushed into the state?
218+
if message_type != "A" {
219+
self.state.disconnect().await;
220+
return Ok(());
221+
}
222+
}
223+
213224
// TODO: should we verify messages here?
214225

215-
let message_type = message.header().get(fix44::MSG_TYPE)?;
216226
match message_type {
217227
"0" => {
218228
self.on_heartbeat(&message).await;
Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,94 @@
11
use hotfix::Message as HotfixMessage;
22
use hotfix::message::FixMessage;
3+
use hotfix_message::{Part, fix44};
34

5+
/// Business messages used for testing.
46
#[derive(Debug, Clone)]
5-
pub struct TestMessage;
7+
pub enum TestMessage {
8+
/// A minimal implementation of a valid execution report.
9+
MinimalExecutionReport {
10+
order_id: String,
11+
exec_id: String,
12+
exec_type: fix44::ExecType,
13+
ord_status: fix44::OrdStatus,
14+
side: fix44::Side,
15+
symbol: String,
16+
order_qty: f64,
17+
price: f64,
18+
},
19+
}
20+
21+
impl TestMessage {
22+
pub fn dummy_execution_report() -> Self {
23+
Self::MinimalExecutionReport {
24+
order_id: "123456789".to_string(),
25+
exec_id: "123456789".to_string(),
26+
exec_type: fix44::ExecType::New,
27+
ord_status: fix44::OrdStatus::New,
28+
side: fix44::Side::Buy,
29+
symbol: "ABC".to_string(),
30+
order_qty: 100.0,
31+
price: 100.0,
32+
}
33+
}
34+
}
635

736
impl FixMessage for TestMessage {
8-
fn write(&self, _msg: &mut HotfixMessage) {}
37+
fn write(&self, msg: &mut HotfixMessage) {
38+
match self {
39+
TestMessage::MinimalExecutionReport {
40+
order_id,
41+
exec_id,
42+
exec_type,
43+
ord_status,
44+
side,
45+
symbol,
46+
order_qty,
47+
price,
48+
} => {
49+
msg.set(fix44::ORDER_ID, order_id.as_str());
50+
msg.set(fix44::EXEC_ID, exec_id.as_str());
51+
msg.set(fix44::EXEC_TYPE, *exec_type);
52+
msg.set(fix44::ORD_STATUS, *ord_status);
53+
msg.set(fix44::SIDE, *side);
54+
msg.set(fix44::SYMBOL, symbol.as_str());
55+
msg.set(fix44::ORDER_QTY, *order_qty);
56+
msg.set(fix44::PRICE, *price);
57+
}
58+
}
59+
}
960

1061
fn message_type(&self) -> &str {
11-
unimplemented!()
62+
match self {
63+
TestMessage::MinimalExecutionReport { .. } => "8",
64+
}
1265
}
1366

14-
fn parse(_msg: &HotfixMessage) -> Self {
15-
TestMessage
67+
fn parse(msg: &HotfixMessage) -> Self {
68+
let msg_type: &str = msg.get(fix44::MSG_TYPE).unwrap();
69+
if msg_type != "8" {
70+
// not an execution report
71+
panic!("Invalid message type: {}", msg_type);
72+
}
73+
74+
let order_id: &str = msg.get(fix44::ORDER_ID).unwrap();
75+
let exec_id: &str = msg.get(fix44::EXEC_ID).unwrap();
76+
let exec_type = msg.get(fix44::EXEC_TYPE).unwrap();
77+
let ord_status = msg.get(fix44::ORD_STATUS).unwrap();
78+
let side = msg.get(fix44::SIDE).unwrap();
79+
let symbol: &str = msg.get(fix44::SYMBOL).unwrap();
80+
let order_qty = msg.get(fix44::ORDER_QTY).unwrap();
81+
let price = msg.get(fix44::PRICE).unwrap();
82+
83+
Self::MinimalExecutionReport {
84+
order_id: order_id.to_string(),
85+
exec_id: exec_id.to_string(),
86+
exec_type,
87+
ord_status,
88+
side,
89+
symbol: symbol.to_string(),
90+
order_qty,
91+
price,
92+
}
1693
}
1794
}

crates/hotfix/tests/session_test_cases/logon_tests.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::common::session_actions::SessionActions;
22
use crate::common::session_assertions::SessionAssertions;
33
use crate::common::setup::given_a_connected_session;
4+
use crate::common::test_messages::TestMessage;
45
use hotfix::session::Status;
56
use hotfix_message::Part;
67
use hotfix_message::fix44::MSG_TYPE;
@@ -25,3 +26,21 @@ async fn test_happy_logon() {
2526
session.when_disconnect_is_requested().await;
2627
mock_counterparty.then_gets_disconnected().await;
2728
}
29+
30+
#[tokio::test]
31+
async fn test_non_logon_response_to_logon() {
32+
let (session, mut mock_counterparty) = given_a_connected_session().await;
33+
34+
// assert that a logon message is received (type 'A')
35+
mock_counterparty
36+
.then_receives(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "A"))
37+
.await;
38+
session.then_status_changes_to(Status::AwaitingLogon).await;
39+
40+
// counterparty sends an execution report without ever responding to our logon
41+
let dummy_report = TestMessage::dummy_execution_report();
42+
mock_counterparty.when_message_is_sent(dummy_report).await;
43+
44+
// we disconnect them as a result
45+
mock_counterparty.then_gets_disconnected().await;
46+
}

0 commit comments

Comments
 (0)