Skip to content

Commit fb20fb1

Browse files
authored
chore: test case for garbled message handling (#180)
* Add test for handling garbled messages * Refactor utility function to manually replace a tag's value in a raw message * Add doc comments for invalid message tests
1 parent 0d97bdb commit fb20fb1

4 files changed

Lines changed: 85 additions & 2 deletions

File tree

crates/hotfix/tests/common/actions.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ impl When<&mut MockCounterparty<TestMessage>> {
3737
self.target.send_message(message).await;
3838
}
3939

40+
pub async fn sends_raw_message(&mut self, raw_message: Vec<u8>) {
41+
self.target.send_raw_message(raw_message).await;
42+
}
43+
4044
pub async fn sends_gap_fill(&mut self, start_seq_no: u64, new_seq_no: u64) {
4145
self.target.send_gap_fill(start_seq_no, new_seq_no).await;
4246
}

crates/hotfix/tests/common/mock_counterparty.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,14 @@ where
9696
let raw_message = generate_message(
9797
&self.session_config.sender_comp_id,
9898
&self.session_config.target_comp_id,
99-
self.sent_messages.len() + 1,
99+
self.next_target_sequence_number(),
100100
message,
101101
)
102102
.expect("failed to generate message");
103+
self.send_raw_message(raw_message).await;
104+
}
105+
106+
pub async fn send_raw_message(&mut self, raw_message: Vec<u8>) {
103107
self.sent_messages.push(raw_message.clone());
104108
self.session_ref
105109
.new_fix_message_received(RawFixMessage::new(raw_message))
@@ -166,6 +170,10 @@ where
166170
}
167171
}
168172

173+
pub fn next_target_sequence_number(&self) -> usize {
174+
self.sent_messages.len() + 1
175+
}
176+
169177
fn create_writer() -> (WriterRef, Receiver<WriterMessage>) {
170178
let (sender, receiver) = mpsc::channel(10);
171179
(WriterRef::new(sender), receiver)

crates/hotfix/tests/common/test_messages.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,23 @@ impl FixMessage for TestMessage {
147147
}
148148
}
149149
}
150+
151+
/// Replaces the value of a field in a raw FIX message.
152+
pub fn replace_field_value(raw_message: &mut Vec<u8>, tag: u32, new_value: &[u8]) {
153+
let tag_bytes = format!("{}=", tag).into_bytes();
154+
155+
if let Some(field_start) = raw_message
156+
.windows(tag_bytes.len())
157+
.position(|window| window == tag_bytes)
158+
{
159+
let value_start = field_start + tag_bytes.len();
160+
if let Some(field_end) = raw_message[value_start..]
161+
.iter()
162+
.position(|&b| b == b'\x01')
163+
{
164+
let value_end = value_start + field_end;
165+
166+
raw_message.splice(value_start..value_end, new_value.iter().cloned());
167+
}
168+
}
169+
}

crates/hotfix/tests/session_test_cases/invalid_message_tests.rs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
use crate::common::actions::when;
22
use crate::common::assertions::then;
33
use crate::common::setup::given_an_active_session;
4-
use hotfix::message::FixMessage;
4+
use crate::common::test_messages::{TestMessage, replace_field_value};
5+
use hotfix::message::{FixMessage, generate_message};
6+
use hotfix::session::Status;
57
use hotfix_message::dict::{FieldLocation, FixDatatype};
68
use hotfix_message::fix44::MSG_TYPE;
79
use hotfix_message::message::Message;
810
use hotfix_message::{HardCodedFixFieldDefinition, Part, fix44};
911

12+
/// Tests that when a counterparty sends a message containing an invalid/unrecognised field,
13+
/// the session rejects the message by sending a Reject (MsgType=3) message back.
1014
#[tokio::test]
1115
async fn test_message_with_invalid_field_gets_rejected() {
1216
let (session, mut mock_counterparty) = given_an_active_session().await;
@@ -22,6 +26,39 @@ async fn test_message_with_invalid_field_gets_rejected() {
2226
then(&mut mock_counterparty).gets_disconnected().await;
2327
}
2428

29+
/// Tests that when a counterparty sends a garbled message with an invalid body length,
30+
/// the session silently ignores it and detects a sequence gap when the next valid message arrives.
31+
#[tokio::test]
32+
async fn test_garbled_message_with_invalid_target_comp_id_gets_ignored() {
33+
let (session, mut mock_counterparty) = given_an_active_session().await;
34+
35+
// counterparty sends a message with invalid body length, which constitutes a garbled message
36+
let garbled_message = build_execution_report_with_incorrect_body_length(
37+
"dummy-acceptor",
38+
"dummy-initiator",
39+
mock_counterparty.next_target_sequence_number(),
40+
);
41+
when(&mut mock_counterparty)
42+
.sends_raw_message(garbled_message)
43+
.await;
44+
45+
// they then send a valid message
46+
when(&mut mock_counterparty)
47+
.sends_message(TestMessage::dummy_execution_report())
48+
.await;
49+
50+
// we then initiate a resend, having skipped the garbled message
51+
then(&mut mock_counterparty)
52+
.receives(|msg| assert_eq!(msg.header().get::<&str>(MSG_TYPE).unwrap(), "2"))
53+
.await;
54+
then(&session)
55+
.status_changes_to(Status::AwaitingResend)
56+
.await;
57+
58+
when(&session).requests_disconnect().await;
59+
then(&mut mock_counterparty).gets_disconnected().await;
60+
}
61+
2562
/// A new order message with an extra, invalid field.
2663
#[derive(Clone, Debug)]
2764
struct ExecutionReportWithInvalidField {
@@ -83,3 +120,17 @@ pub const CUSTOM_FIELD: &HardCodedFixFieldDefinition = &HardCodedFixFieldDefinit
83120
data_type: FixDatatype::String,
84121
location: FieldLocation::Body,
85122
};
123+
124+
fn build_execution_report_with_incorrect_body_length(
125+
sender_comp_id: &str,
126+
target_comp_id: &str,
127+
msg_seq_num: usize,
128+
) -> Vec<u8> {
129+
let report = TestMessage::dummy_execution_report();
130+
let mut raw_message =
131+
generate_message(sender_comp_id, target_comp_id, msg_seq_num, report).unwrap();
132+
133+
replace_field_value(&mut raw_message, 9, b"999");
134+
135+
raw_message
136+
}

0 commit comments

Comments
 (0)