From 8abe28cec70668c8e28dae6766d45f37f83bcbce Mon Sep 17 00:00:00 2001 From: Vladyslav Bukatin Date: Tue, 31 Mar 2026 14:59:39 +0100 Subject: [PATCH 1/4] Issue 332: allow to skip orig time check for admin messages --- crates/hotfix/src/config.rs | 76 ++++ crates/hotfix/src/initiator.rs | 2 + crates/hotfix/src/message/verification.rs | 417 ++++++++++++++++-- crates/hotfix/src/session.rs | 4 +- crates/hotfix/src/session/ctx.rs | 5 + crates/hotfix/src/session/test_utils.rs | 3 +- .../connection_test_cases/connect_tests.rs | 3 +- .../tests/session_test_cases/common/setup.rs | 3 +- examples/load-testing/src/main.rs | 3 +- 9 files changed, 466 insertions(+), 50 deletions(-) diff --git a/crates/hotfix/src/config.rs b/crates/hotfix/src/config.rs index 858516b3..2c2f7fd4 100644 --- a/crates/hotfix/src/config.rs +++ b/crates/hotfix/src/config.rs @@ -114,6 +114,30 @@ pub struct SessionConfig { /// The schedule configuration for the session pub schedule: Option, + + /// The validation configuration for the session + #[serde(default)] + pub verification: VerificationConfig, +} + +#[derive(Clone, Debug, Deserialize)] +/// The configuration of validation rules. +pub struct VerificationConfig { + /// Specifies whether we should check the original sending time for admin messages. + #[serde(default = "default_true")] + pub check_orig_sending_time_for_admin: bool, +} + +impl Default for VerificationConfig { + fn default() -> Self { + Self { + check_orig_sending_time_for_admin: true, + } + } +} + +fn default_true() -> bool { + true } /// Errors that may occur when loading configuration. @@ -170,6 +194,11 @@ reset_on_logon = false assert_eq!(session_config.tls_config, Some(expected_tls_config)); assert_eq!(session_config.reconnect_interval, 30); assert_eq!(session_config.logon_timeout, 10); + assert!( + session_config + .verification + .check_orig_sending_time_for_admin + ); } #[test] @@ -439,6 +468,53 @@ end_day = "Friday" assert_eq!(session_config.reconnect_interval, 15); } + #[test] + fn test_verification_config_defaults_when_omitted() { + let config_contents = r#" +[[sessions]] +begin_string = "FIX.4.4" +sender_comp_id = "send-comp-id" +target_comp_id = "target-comp-id" +connection_port = 443 +connection_host = "127.0.0.1" +heartbeat_interval = 30 + "#; + + let config: Config = toml::from_str(config_contents).unwrap(); + let session_config = config.sessions.first().unwrap(); + + assert!( + session_config + .verification + .check_orig_sending_time_for_admin + ); + } + + #[test] + fn test_verification_config_can_disable_admin_orig_sending_time_check() { + let config_contents = r#" +[[sessions]] +begin_string = "FIX.4.4" +sender_comp_id = "send-comp-id" +target_comp_id = "target-comp-id" +connection_port = 443 +connection_host = "127.0.0.1" +heartbeat_interval = 30 + +[sessions.verification] +check_orig_sending_time_for_admin = false + "#; + + let config: Config = toml::from_str(config_contents).unwrap(); + let session_config = config.sessions.first().unwrap(); + + assert!( + !session_config + .verification + .check_orig_sending_time_for_admin + ); + } + #[test] fn test_load_from_path_success() { let config_contents = r#" diff --git a/crates/hotfix/src/initiator.rs b/crates/hotfix/src/initiator.rs index 7e64adf3..b78a9606 100644 --- a/crates/hotfix/src/initiator.rs +++ b/crates/hotfix/src/initiator.rs @@ -157,6 +157,7 @@ async fn establish_connection( mod tests { use super::*; use crate::application::{Application, InboundDecision, OutboundDecision}; + use crate::config::VerificationConfig; use crate::message::generate_message; use crate::message::logon::{Logon, ResetSeqNumConfig}; use crate::message::logout::Logout; @@ -299,6 +300,7 @@ mod tests { reconnect_interval: 1, // Short for tests reset_on_logon: false, schedule: None, + verification: VerificationConfig::default(), } } diff --git a/crates/hotfix/src/message/verification.rs b/crates/hotfix/src/message/verification.rs index a1ed3546..922a0423 100644 --- a/crates/hotfix/src/message/verification.rs +++ b/crates/hotfix/src/message/verification.rs @@ -1,9 +1,9 @@ -use crate::config::SessionConfig; -use crate::message::ResendRequest; +use crate::config::{SessionConfig, VerificationConfig}; use crate::message::logout::Logout; use crate::message::reject::Reject; use crate::message::sequence_reset::SequenceReset; use crate::message::verification_issue::{CompIdType, MessageError, VerificationIssue}; +use crate::message::{ResendRequest, is_admin}; use crate::session::error::SessionOperationError; use hotfix_message::Part; use hotfix_message::field_types::Timestamp; @@ -22,13 +22,20 @@ const SENDING_TIME_THRESHOLD: u64 = 120; pub(crate) struct VerificationFlags { pub(crate) check_too_high: bool, pub(crate) check_too_low: bool, + #[allow(dead_code)] + pub(crate) check_orig_sending_time: bool, } impl VerificationFlags { - pub(crate) fn new(check_too_high: bool, check_too_low: bool) -> Self { + pub(crate) fn new( + check_too_high: bool, + check_too_low: bool, + check_orig_sending_time: bool, + ) -> Self { Self { check_too_high, check_too_low, + check_orig_sending_time, } } @@ -36,24 +43,36 @@ impl VerificationFlags { self.check_too_high || self.check_too_low } - pub(crate) fn for_message(message: &Message) -> Result { + pub(crate) fn for_message( + message: &Message, + user_config: &VerificationConfig, + ) -> Result { let message_type: &str = message .header() .get(MSG_TYPE) .map_err(|_| SessionOperationError::MissingField("MSG_TYPE"))?; + let possible_duplicate = message.header().get::(POSS_DUP_FLAG).unwrap_or(false); + let check_orig_sending_time = if is_admin(message_type) { + possible_duplicate && user_config.check_orig_sending_time_for_admin + } else { + possible_duplicate + }; + let flags = match message_type { // check_too_high=false: QFJ-673 deadlock fix. When both sides send // ResendRequest simultaneously, each side's ResendRequest will have a seq // number higher than expected. By not treating that as an error, we allow // the ResendRequest to be processed. - ResendRequest::MSG_TYPE | Reject::MSG_TYPE => Self::new(false, true), - Logout::MSG_TYPE => Self::new(false, false), + ResendRequest::MSG_TYPE | Reject::MSG_TYPE => { + Self::new(false, true, check_orig_sending_time) + } + Logout::MSG_TYPE => Self::new(false, false, check_orig_sending_time), SequenceReset::MSG_TYPE => { let is_gap_fill: bool = message.get(GAP_FILL_FLAG).unwrap_or(false); - Self::new(is_gap_fill, is_gap_fill) + Self::new(is_gap_fill, is_gap_fill, check_orig_sending_time) } - _ => Self::new(true, true), + _ => Self::new(true, true, check_orig_sending_time), }; Ok(flags) @@ -80,7 +99,7 @@ pub(crate) fn verify_message( // check SendingTime and OrigSendingTime let sending_time = check_sending_time(message, actual_seq_number)?; let possible_duplicate = message.header().get::(POSS_DUP_FLAG).unwrap_or(false); - if possible_duplicate { + if flags.check_orig_sending_time { check_original_sending_time(message, actual_seq_number, sending_time)?; } @@ -227,7 +246,8 @@ fn check_target_comp_id( #[cfg(test)] mod tests { - use super::{Message, SessionConfig, VerificationFlags, verify_message}; + use super::{Message, SessionConfig, VerificationConfig, VerificationFlags, verify_message}; + use crate::message::heartbeat::Heartbeat; use crate::message::verification_issue::{CompIdType, MessageError, VerificationIssue}; use hotfix_message::field_types::Timestamp; use hotfix_message::{Part, fix44}; @@ -247,6 +267,7 @@ mod tests { reconnect_interval: 0, reset_on_logon: false, schedule: None, + verification: VerificationConfig::default(), } } @@ -256,7 +277,17 @@ mod tests { target_comp_id: &str, seq_num: u64, ) -> Message { - let mut msg = Message::new(begin_string, "D"); + build_test_message_with_type(begin_string, "D", sender_comp_id, target_comp_id, seq_num) + } + + fn build_test_message_with_type( + begin_string: &str, + message_type: &str, + sender_comp_id: &str, + target_comp_id: &str, + seq_num: u64, + ) -> Message { + let mut msg = Message::new(begin_string, message_type); msg.set(fix44::SENDER_COMP_ID, sender_comp_id); msg.set(fix44::TARGET_COMP_ID, target_comp_id); msg.set(fix44::MSG_SEQ_NUM, seq_num); @@ -264,12 +295,97 @@ mod tests { msg } + #[test] + fn test_creating_flags_for_admin_message_should_check_orig_sending_time_when_enabled() { + let mut msg = + build_test_message_with_type("FIX.4.4", Heartbeat::MSG_TYPE, "TARGET", "SENDER", 42); + msg.header_mut().set(fix44::POSS_DUP_FLAG, true); + + let flags = VerificationFlags::for_message(&msg, &VerificationConfig::default()).unwrap(); + + assert!(flags.check_orig_sending_time); + assert!(flags.check_too_high); + assert!(flags.check_too_low); + } + + #[test] + fn test_creating_flags_for_admin_message_should_skip_check_orig_sending_time_when_not_poss_dup() + { + let msg = + build_test_message_with_type("FIX.4.4", Heartbeat::MSG_TYPE, "TARGET", "SENDER", 42); + + let flags = VerificationFlags::for_message(&msg, &VerificationConfig::default()).unwrap(); + + assert!(!flags.check_orig_sending_time); + assert!(flags.check_too_high); + assert!(flags.check_too_low); + } + + #[test] + fn test_creating_flags_for_admin_message_should_skip_check_orig_sending_time_when_disabled() { + let mut msg = + build_test_message_with_type("FIX.4.4", Heartbeat::MSG_TYPE, "TARGET", "SENDER", 42); + msg.header_mut().set(fix44::POSS_DUP_FLAG, true); + + let flags = VerificationFlags::for_message( + &msg, + &VerificationConfig { + check_orig_sending_time_for_admin: false, + }, + ) + .unwrap(); + + assert!(!flags.check_orig_sending_time); + assert!(flags.check_too_high); + assert!(flags.check_too_low); + } + + #[test] + fn test_creating_flags_for_app_messages_does_not_skip_orig_sending_time() { + let mut msg = build_test_message("FIX.4.4", "TARGET", "SENDER", 42); + msg.header_mut().set(fix44::POSS_DUP_FLAG, true); + + let flags = VerificationFlags::for_message( + &msg, + &VerificationConfig { + check_orig_sending_time_for_admin: false, + }, + ) + .unwrap(); + + assert!(flags.check_orig_sending_time); + assert!(flags.check_too_high); + assert!(flags.check_too_low); + } + + #[test] + fn test_creating_flags_for_app_messages_skip_orig_sending_time_when_it_is_not_poss_dup() { + let msg = build_test_message("FIX.4.4", "TARGET", "SENDER", 42); + + let flags = VerificationFlags::for_message( + &msg, + &VerificationConfig { + check_orig_sending_time_for_admin: true, + }, + ) + .unwrap(); + + assert!(!flags.check_orig_sending_time); + assert!(flags.check_too_high); + assert!(flags.check_too_low); + } + #[test] fn test_verify_message_happy_path() { let config = build_test_config(); let msg = build_test_message("FIX.4.4", "TARGET", "SENDER", 42); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, false), + ); assert!(result.is_ok()); } @@ -279,7 +395,12 @@ mod tests { let config = build_test_config(); let msg = build_test_message("FIX.4.2", "TARGET", "SENDER", 42); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, false), + ); assert!(matches!( result, @@ -300,7 +421,12 @@ mod tests { let config = build_test_config(); let msg = build_test_message("FIX.4.4", "WRONG_SENDER", "SENDER", 42); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, false), + ); assert!(matches!( result, @@ -328,7 +454,12 @@ mod tests { let config = build_test_config(); let msg = build_test_message("FIX.4.4", "TARGET", "WRONG_TARGET", 42); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, false), + ); assert!(matches!( result, @@ -356,7 +487,12 @@ mod tests { let config = build_test_config(); let msg = build_test_message("FIX.4.4", "TARGET", "SENDER", 40); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, false), + ); assert!(matches!( result, @@ -384,7 +520,43 @@ mod tests { msg.header_mut().set(fix44::POSS_DUP_FLAG, true); msg.header_mut().set(fix44::ORIG_SENDING_TIME, sending_time); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, true), + ); + + assert!(matches!( + result, + Err(VerificationIssue::InvalidMessage( + MessageError::SeqNumberTooLow { .. } + )) + )); + if let Err(VerificationIssue::InvalidMessage(MessageError::SeqNumberTooLow { + expected, + actual, + possible_duplicate, + })) = result + { + assert_eq!(expected, 42); + assert_eq!(actual, 40); + assert!(possible_duplicate); + } + } + + #[test] + fn test_seq_number_too_low_with_poss_dup_flag_without_orig_time() { + let config = build_test_config(); + let mut msg = build_test_message("FIX.4.4", "TARGET", "SENDER", 40); + msg.header_mut().set(fix44::POSS_DUP_FLAG, true); + + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, false), + ); assert!(matches!( result, @@ -409,7 +581,12 @@ mod tests { let config = build_test_config(); let msg = build_test_message("FIX.4.4", "TARGET", "SENDER", 50); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, false), + ); assert!(matches!(result, Err(VerificationIssue::SequenceGap { .. }))); if let Err(VerificationIssue::SequenceGap { expected, actual }) = result { @@ -425,7 +602,12 @@ mod tests { msg.header_mut().set(fix44::POSS_DUP_FLAG, true); // Don't set OrigSendingTime - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, true), + ); assert!(matches!( result, @@ -455,7 +637,34 @@ mod tests { msg.header_mut().pop(fix44::SENDING_TIME); msg.header_mut().set(fix44::SENDING_TIME, sending_time); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, true), + ); + + assert!(result.is_ok()); + } + + #[test] + fn test_check_orig_time_is_skippen_when_flag_is_turned_off() { + let config = build_test_config(); + let mut msg = build_test_message("FIX.4.4", "TARGET", "SENDER", 42); + + std::thread::sleep(std::time::Duration::from_millis(10)); + let sending_time = Timestamp::utc_now(); + + msg.header_mut().set(fix44::POSS_DUP_FLAG, true); + msg.header_mut().pop(fix44::SENDING_TIME); + msg.header_mut().set(fix44::SENDING_TIME, sending_time); + + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, false), + ); assert!(result.is_ok()); } @@ -474,7 +683,12 @@ mod tests { msg.header_mut().pop(fix44::SENDING_TIME); msg.header_mut().set(fix44::SENDING_TIME, sending_time); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, true), + ); assert!(matches!( result, @@ -495,6 +709,29 @@ mod tests { } } + #[test] + fn test_check_orig_sending_time_after_sending_time_is_skipped_when_flag_turned_off() { + let config = build_test_config(); + let mut msg = build_test_message("FIX.4.4", "TARGET", "SENDER", 42); + + let sending_time = Timestamp::utc_now(); + std::thread::sleep(std::time::Duration::from_millis(10)); + let orig_time = Timestamp::utc_now(); + + msg.header_mut().set(fix44::POSS_DUP_FLAG, true); + msg.header_mut().set(fix44::ORIG_SENDING_TIME, orig_time); + msg.header_mut().pop(fix44::SENDING_TIME); + msg.header_mut().set(fix44::SENDING_TIME, sending_time); + + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, false), + ); + assert!(result.is_ok()); + } + #[test] fn test_poss_dup_flag_with_equal_timestamps() { let config = build_test_config(); @@ -508,7 +745,12 @@ mod tests { msg.header_mut().pop(fix44::SENDING_TIME); msg.header_mut().set(fix44::SENDING_TIME, timestamp); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, true), + ); // equal timestamps should be valid (orig <= sending) assert!(result.is_ok()); @@ -526,7 +768,12 @@ mod tests { // remove begin string, which is automatically added by `Message::new` msg.header_mut().pop(fix44::BEGIN_STRING); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, false), + ); assert!(matches!( result, @@ -544,7 +791,12 @@ mod tests { msg.set(fix44::MSG_SEQ_NUM, 42u64); msg.set(fix44::SENDING_TIME, Timestamp::utc_now()); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, false), + ); assert!(matches!( result, @@ -565,7 +817,12 @@ mod tests { msg.set(fix44::MSG_SEQ_NUM, 42u64); msg.set(fix44::SENDING_TIME, Timestamp::utc_now()); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, false), + ); assert!(matches!( result, @@ -586,7 +843,12 @@ mod tests { msg.set(fix44::TARGET_COMP_ID, "SENDER"); msg.set(fix44::SENDING_TIME, Timestamp::utc_now()); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, false), + ); // missing seq num defaults to 0, which will be too low assert!(matches!( @@ -602,7 +864,12 @@ mod tests { let config = build_test_config(); let msg = build_test_message("FIX.4.4", "TARGET", "SENDER", 0); - let result = verify_message(&msg, &config, Some(1), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(1), + VerificationFlags::new(true, true, false), + ); assert!(matches!( result, @@ -617,7 +884,12 @@ mod tests { let config = build_test_config(); let msg = build_test_message("FIX.4.4", "TARGET", "SENDER", 1); - let result = verify_message(&msg, &config, Some(1), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(1), + VerificationFlags::new(true, true, false), + ); assert!(result.is_ok()); } @@ -628,7 +900,12 @@ mod tests { // wrong begin string AND wrong seq num - begin string error should come first let msg = build_test_message("FIX.4.2", "TARGET", "SENDER", 100); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, false), + ); assert!(matches!( result, @@ -644,7 +921,12 @@ mod tests { // wrong sender and wrong target - sender error should come first let msg = build_test_message("FIX.4.4", "WRONG_SENDER", "WRONG_TARGET", 42); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, false), + ); assert!(matches!( result, @@ -665,7 +947,12 @@ mod tests { msg.set(fix44::TARGET_COMP_ID, "SENDER"); msg.set(fix44::MSG_SEQ_NUM, 42u64); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, false), + ); assert!(matches!( result, @@ -698,7 +985,12 @@ mod tests { let past_timestamp: Timestamp = past_time.naive_utc().into(); msg.set(fix44::SENDING_TIME, past_timestamp); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, false), + ); assert!(matches!( result, @@ -731,7 +1023,12 @@ mod tests { let future_timestamp: Timestamp = future_time.naive_utc().into(); msg.set(fix44::SENDING_TIME, future_timestamp); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, false), + ); assert!(matches!( result, @@ -763,7 +1060,12 @@ mod tests { let boundary_timestamp: Timestamp = boundary_time.naive_utc().into(); msg.set(fix44::SENDING_TIME, boundary_timestamp); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, false), + ); assert!(result.is_ok()); } @@ -784,7 +1086,12 @@ mod tests { let valid_timestamp: Timestamp = valid_time.naive_utc().into(); msg.set(fix44::SENDING_TIME, valid_timestamp); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, true, false), + ); assert!(result.is_ok()); } @@ -795,7 +1102,12 @@ mod tests { let msg = build_test_message("FIX.4.4", "TARGET", "SENDER", 50); // With check_too_high=false, seq 50 > expected 42 should be OK - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(false, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(false, true, false), + ); assert!(result.is_ok()); } @@ -806,13 +1118,18 @@ mod tests { let msg = build_test_message("FIX.4.4", "TARGET", "SENDER", 40); // With check_too_low=false, seq 40 < expected 42 should be OK - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, false)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, false, false), + ); assert!(result.is_ok()); } #[test] - fn test_both_checks_disabled() { + fn test_all_checks_disabled() { let config = build_test_config(); // Seq number too high let msg_high = build_test_message("FIX.4.4", "TARGET", "SENDER", 50); @@ -821,7 +1138,7 @@ mod tests { &msg_high, &config, Some(42), - VerificationFlags::new(false, false) + VerificationFlags::new(false, false, false) ) .is_ok() ); @@ -833,7 +1150,7 @@ mod tests { &msg_low, &config, Some(42), - VerificationFlags::new(false, false) + VerificationFlags::new(false, false, false) ) .is_ok() ); @@ -845,7 +1162,7 @@ mod tests { &msg_match, &config, Some(42), - VerificationFlags::new(false, false) + VerificationFlags::new(false, false, false) ) .is_ok() ); @@ -856,7 +1173,12 @@ mod tests { let config = build_test_config(); let msg = build_test_message("FIX.4.4", "TARGET", "SENDER", 50); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(true, false)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(true, false, false), + ); assert!(matches!(result, Err(VerificationIssue::SequenceGap { .. }))); } @@ -866,7 +1188,12 @@ mod tests { let config = build_test_config(); let msg = build_test_message("FIX.4.4", "TARGET", "SENDER", 40); - let result = verify_message(&msg, &config, Some(42), VerificationFlags::new(false, true)); + let result = verify_message( + &msg, + &config, + Some(42), + VerificationFlags::new(false, true, false), + ); assert!(matches!( result, @@ -886,7 +1213,7 @@ mod tests { &msg, &config, Some(42), - VerificationFlags::new(false, false), + VerificationFlags::new(false, false, false), ); assert!(matches!( result, @@ -901,7 +1228,7 @@ mod tests { &msg, &config, Some(42), - VerificationFlags::new(false, false), + VerificationFlags::new(false, false, false), ); assert!(matches!( result, diff --git a/crates/hotfix/src/session.rs b/crates/hotfix/src/session.rs index 21fcb3fe..e99e7fc9 100644 --- a/crates/hotfix/src/session.rs +++ b/crates/hotfix/src/session.rs @@ -208,7 +208,7 @@ where .get(MSG_TYPE) .map_err(|_| SessionOperationError::MissingField("MSG_TYPE"))?; - let flags = VerificationFlags::for_message(&message)?; + let flags = VerificationFlags::for_message(&message, self.ctx.verification_config())?; if let VerificationResult::Issue(result) = self .state .handle_verification_issue(&mut self.ctx, &message, flags) @@ -755,6 +755,7 @@ async fn run_session( mod tests { use super::*; use crate::application::{InboundDecision, OutboundDecision}; + use crate::config::VerificationConfig; use crate::message::OutboundMessage; use crate::store::{Result as StoreResult, StoreError}; use chrono::{DateTime, Datelike, NaiveDate, NaiveTime, TimeDelta, Timelike}; @@ -891,6 +892,7 @@ mod tests { reconnect_interval: 30, reset_on_logon: false, schedule: None, + verification: VerificationConfig::default(), } } diff --git a/crates/hotfix/src/session/ctx.rs b/crates/hotfix/src/session/ctx.rs index 75c2fa5e..729dd240 100644 --- a/crates/hotfix/src/session/ctx.rs +++ b/crates/hotfix/src/session/ctx.rs @@ -3,6 +3,7 @@ use hotfix_message::message::Config as MessageConfig; use hotfix_store::MessageStore; use crate::config::SessionConfig; +use crate::config::VerificationConfig; use crate::message::OutboundMessage; use crate::message::generate_message; use crate::message::parser::RawFixMessage; @@ -88,4 +89,8 @@ impl SessionCtx { raw: RawFixMessage::new(msg), }) } + + pub fn verification_config(&self) -> &VerificationConfig { + &self.config.verification + } } diff --git a/crates/hotfix/src/session/test_utils.rs b/crates/hotfix/src/session/test_utils.rs index d71becc0..006907f5 100644 --- a/crates/hotfix/src/session/test_utils.rs +++ b/crates/hotfix/src/session/test_utils.rs @@ -1,4 +1,4 @@ -use crate::config::SessionConfig; +use crate::config::{SessionConfig, VerificationConfig}; use crate::session::ctx::SessionCtx; use crate::store::{MessageStore, Result as StoreResult}; use crate::transport::writer::{WriterMessage, WriterRef}; @@ -87,6 +87,7 @@ pub(crate) fn create_test_ctx(store: FakeMessageStore) -> SessionCtx<(), FakeMes reconnect_interval: 30, reset_on_logon: false, schedule: None, + verification: VerificationConfig::default(), }, store, application: (), diff --git a/crates/hotfix/tests/connection_test_cases/connect_tests.rs b/crates/hotfix/tests/connection_test_cases/connect_tests.rs index 4e677101..918978a0 100644 --- a/crates/hotfix/tests/connection_test_cases/connect_tests.rs +++ b/crates/hotfix/tests/connection_test_cases/connect_tests.rs @@ -6,7 +6,7 @@ use crate::helpers::{ MinimalApplication, MinimalMessage, TestCertificates, TestTcpServer, TestTlsServer, }; -use hotfix::config::{SessionConfig, TlsConfig}; +use hotfix::config::{SessionConfig, TlsConfig, VerificationConfig}; use hotfix::session::InternalSessionRef; use hotfix::store::in_memory::InMemoryMessageStore; use hotfix::transport::socket::connect; @@ -26,6 +26,7 @@ fn create_session_config(host: &str, port: u16, tls_config: Option) - reconnect_interval: 30, reset_on_logon: false, schedule: None, + verification: VerificationConfig::default(), } } diff --git a/crates/hotfix/tests/session_test_cases/common/setup.rs b/crates/hotfix/tests/session_test_cases/common/setup.rs index b6faeea7..ecf464fa 100644 --- a/crates/hotfix/tests/session_test_cases/common/setup.rs +++ b/crates/hotfix/tests/session_test_cases/common/setup.rs @@ -3,7 +3,7 @@ use crate::common::assertions::then; use crate::common::fakes::{FakeApplication, FakeCounterparty, SessionSpy}; use crate::common::test_messages::TestMessage; use crate::session_test_cases::common::fakes::DisconnectedSession; -use hotfix::config::SessionConfig; +use hotfix::config::{SessionConfig, VerificationConfig}; use hotfix::session::InternalSessionRef; use hotfix::session::Status; use hotfix::store::in_memory::InMemoryMessageStore; @@ -119,6 +119,7 @@ pub fn create_session_config() -> SessionConfig { reconnect_interval: 30, reset_on_logon: false, schedule: None, + verification: VerificationConfig::default(), } } diff --git a/examples/load-testing/src/main.rs b/examples/load-testing/src/main.rs index 976f05b1..7fb2962e 100644 --- a/examples/load-testing/src/main.rs +++ b/examples/load-testing/src/main.rs @@ -3,7 +3,7 @@ mod messages; use anyhow::Result; use clap::{Parser, ValueEnum}; -use hotfix::config::SessionConfig; +use hotfix::config::{SessionConfig, VerificationConfig}; use hotfix::field_types::{Date, Timestamp}; use hotfix::fix44; use hotfix::fix44::OrdType; @@ -168,5 +168,6 @@ fn get_config() -> SessionConfig { reconnect_interval: 30, reset_on_logon: true, schedule: None, + verification: VerificationConfig::default(), } } From 76e4c238f424bce2153096619de77b62bce8756a Mon Sep 17 00:00:00 2001 From: Vladyslav Bukatin Date: Tue, 31 Mar 2026 15:56:57 +0100 Subject: [PATCH 2/4] 322: create builder for verification config to maintain version compatibility in future --- crates/hotfix/CHANGELOG.md | 3 +++ crates/hotfix/src/config.rs | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/crates/hotfix/CHANGELOG.md b/crates/hotfix/CHANGELOG.md index c5a0a34f..94f31c23 100644 --- a/crates/hotfix/CHANGELOG.md +++ b/crates/hotfix/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + - possibility to disalbe original sending time(tag 122) for admin messages([#322](https://github.com/Boku-Labs/boku-hotfix/pull/3)) + ## [0.11.0](https://github.com/Validus-Risk-Management/hotfix/compare/hotfix-v0.10.0...hotfix-v0.11.0) - 2026-03-25 ### Added diff --git a/crates/hotfix/src/config.rs b/crates/hotfix/src/config.rs index 2c2f7fd4..ba435866 100644 --- a/crates/hotfix/src/config.rs +++ b/crates/hotfix/src/config.rs @@ -128,7 +128,23 @@ pub struct VerificationConfig { pub check_orig_sending_time_for_admin: bool, } +impl VerificationConfig { + pub fn builder() -> VerificationConfigBuilder { + VerificationConfigBuilder::default() + } +} + impl Default for VerificationConfig { + fn default() -> Self { + VerificationConfigBuilder::default().build() + } +} + +pub struct VerificationConfigBuilder { + check_orig_sending_time_for_admin: bool, +} + +impl Default for VerificationConfigBuilder { fn default() -> Self { Self { check_orig_sending_time_for_admin: true, @@ -136,6 +152,23 @@ impl Default for VerificationConfig { } } +impl VerificationConfigBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn check_orig_sending_time_for_admin(mut self, value: bool) -> Self { + self.check_orig_sending_time_for_admin = value; + self + } + + pub fn build(self) -> VerificationConfig { + VerificationConfig { + check_orig_sending_time_for_admin: self.check_orig_sending_time_for_admin, + } + } +} + fn default_true() -> bool { true } From ea713301d7a4bbe6ab241770447bca6cde379ca8 Mon Sep 17 00:00:00 2001 From: Vladyslav Bukatin Date: Tue, 31 Mar 2026 17:02:38 +0100 Subject: [PATCH 3/4] Add possibility to skip upload to codecov --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e6f92ff..e3c5e5f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,7 @@ jobs: with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} + dry_run: ${{ vars.CODECOV_DRY_RUN == 'true'}} flags: core files: lcov.info @@ -71,5 +72,6 @@ jobs: with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} + dry_run: ${{ vars.CODECOV_DRY_RUN == 'true'}} flags: mongodb files: lcov.info From b86c740a084ec33080fb5c1f4e91ab663cba9949 Mon Sep 17 00:00:00 2001 From: Vladyslav Bukatin Date: Tue, 31 Mar 2026 20:31:50 +0100 Subject: [PATCH 4/4] Fix: change log to include proper PR --- crates/hotfix/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/hotfix/CHANGELOG.md b/crates/hotfix/CHANGELOG.md index 94f31c23..933f33b1 100644 --- a/crates/hotfix/CHANGELOG.md +++ b/crates/hotfix/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - - possibility to disalbe original sending time(tag 122) for admin messages([#322](https://github.com/Boku-Labs/boku-hotfix/pull/3)) + - possibility to disalbe original sending time(tag 122) for admin messages([#322](https://github.com/Validus-Risk-Management/hotfix/pull/334)) ## [0.11.0](https://github.com/Validus-Risk-Management/hotfix/compare/hotfix-v0.10.0...hotfix-v0.11.0) - 2026-03-25