Skip to content

Commit a5655a0

Browse files
committed
Add rejection tests for set target sequence number action
1 parent 368014d commit a5655a0

3 files changed

Lines changed: 235 additions & 13 deletions

File tree

crates/hotfix/src/session.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,11 @@ where
574574
.try_set_next_target_seq_num(&mut self.ctx, seq_num)
575575
.await;
576576
if let Err(ref err) = response {
577-
warn!(?err, seq_num = seq_num.get(), "SetNextTargetSeqNum rejected");
577+
warn!(
578+
?err,
579+
seq_num = seq_num.get(),
580+
"SetNextTargetSeqNum rejected"
581+
);
578582
}
579583
if responder.send(response).is_err() {
580584
error!("failed to respond to SetNextTargetSeqNum request");

crates/hotfix/src/session/state.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -250,12 +250,7 @@ impl SessionState {
250250
}
251251
}
252252

253-
/// Set the next expected target sequence number. Only permitted while
254-
/// `Disconnected` — any other state returns `InvalidState`.
255-
///
256-
/// The store stores "last seen" (see `inbound::on_sequence_reset` passing
257-
/// `end - 1`), so we subtract 1 to make `next_target_seq_number()` return
258-
/// `seq_num`. `NonZeroU64` guarantees the subtraction is safe.
253+
/// Set the next expected target sequence number.
259254
pub(crate) async fn try_set_next_target_seq_num<A, S>(
260255
&self,
261256
ctx: &mut SessionCtx<A, S>,
@@ -265,12 +260,18 @@ impl SessionState {
265260
A: Application,
266261
S: MessageStore,
267262
{
263+
// Only permitted while `Disconnected` — any other state returns `InvalidState`.
268264
match self {
269-
SessionState::Disconnected(_) => ctx
270-
.store
271-
.set_target_seq_number(seq_num.get() - 1)
272-
.await
273-
.map_err(SetNextTargetSeqNumError::from),
265+
SessionState::Disconnected(_) => {
266+
// The store stores "last seen" (see `inbound::on_sequence_reset` passing `end - 1`),
267+
// so we subtract 1 to make `next_target_seq_number()` return `seq_num`.
268+
let target_seq_num = seq_num.get() - 1;
269+
270+
ctx.store
271+
.set_target_seq_number(target_seq_num)
272+
.await
273+
.map_err(SetNextTargetSeqNumError::from)
274+
}
274275
_ => Err(SetNextTargetSeqNumError::InvalidState {
275276
current: self.as_status(),
276277
}),

crates/hotfix/tests/session_test_cases/admin_request_tests.rs

Lines changed: 218 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::common::actions::when;
22
use crate::common::assertions::{assert_msg_type, then};
33
use crate::common::cleanup::finally;
44
use crate::common::setup::given_an_active_session;
5-
use hotfix::session::Status;
5+
use hotfix::session::{SetNextTargetSeqNumError, Status};
66
use hotfix_message::Part;
77
use hotfix_message::fix44::{MsgType, RESET_SEQ_NUM_FLAG};
88
use std::num::NonZeroU64;
@@ -94,3 +94,220 @@ async fn test_set_next_target_seq_num_while_disconnected() {
9494
.expect("session info");
9595
assert_eq!(info.next_target_seq_number, 42);
9696
}
97+
98+
/// Rejection: while `Active`, SetNextTargetSeqNum is refused and the store is
99+
/// untouched.
100+
#[tokio::test]
101+
async fn test_set_next_target_seq_num_rejected_while_active() {
102+
let (session, mut counterparty) = crate::common::setup::given_an_active_session().await;
103+
104+
let info_before = session
105+
.session_handle()
106+
.get_session_info()
107+
.await
108+
.expect("session info");
109+
110+
let result = session
111+
.session_handle()
112+
.set_next_target_seq_num(NonZeroU64::new(42).expect("42 is non-zero"))
113+
.await;
114+
115+
assert!(
116+
matches!(
117+
result,
118+
Err(SetNextTargetSeqNumError::InvalidState {
119+
current: Status::Active
120+
})
121+
),
122+
"expected InvalidState{{ current: Active }}, got: {result:?}"
123+
);
124+
125+
let info_after = session
126+
.session_handle()
127+
.get_session_info()
128+
.await
129+
.expect("session info");
130+
assert_eq!(
131+
info_after.next_target_seq_number, info_before.next_target_seq_number,
132+
"target sequence number should not change on rejection"
133+
);
134+
135+
crate::common::cleanup::finally(&session, &mut counterparty)
136+
.disconnect()
137+
.await;
138+
}
139+
140+
/// Rejection: while `AwaitingLogon` (we've sent our Logon, peer hasn't responded),
141+
/// SetNextTargetSeqNum is refused and the store is untouched.
142+
#[tokio::test]
143+
async fn test_set_next_target_seq_num_rejected_while_awaiting_logon() {
144+
let (session, mut counterparty) = crate::common::setup::given_a_connected_session().await;
145+
146+
// wait for our outbound Logon so we're deterministically in AwaitingLogon
147+
crate::common::assertions::then(&mut counterparty)
148+
.receives(|msg| {
149+
crate::common::assertions::assert_msg_type(msg, hotfix_message::fix44::MsgType::Logon)
150+
})
151+
.await;
152+
153+
let info_before = session
154+
.session_handle()
155+
.get_session_info()
156+
.await
157+
.expect("session info");
158+
159+
let result = session
160+
.session_handle()
161+
.set_next_target_seq_num(NonZeroU64::new(42).expect("42 is non-zero"))
162+
.await;
163+
164+
assert!(
165+
matches!(
166+
result,
167+
Err(SetNextTargetSeqNumError::InvalidState {
168+
current: Status::AwaitingLogon
169+
})
170+
),
171+
"expected InvalidState{{ current: AwaitingLogon }}, got: {result:?}"
172+
);
173+
174+
let info_after = session
175+
.session_handle()
176+
.get_session_info()
177+
.await
178+
.expect("session info");
179+
assert_eq!(
180+
info_after.next_target_seq_number,
181+
info_before.next_target_seq_number
182+
);
183+
184+
crate::common::cleanup::finally(&session, &mut counterparty)
185+
.disconnect()
186+
.await;
187+
}
188+
189+
/// Rejection: while `AwaitingLogout` (we've sent our Logout and are waiting
190+
/// for the peer's reply), SetNextTargetSeqNum is refused and the store is
191+
/// untouched.
192+
#[tokio::test]
193+
async fn test_set_next_target_seq_num_rejected_while_awaiting_logout() {
194+
use crate::common::actions::when;
195+
use crate::common::assertions::{assert_msg_type, then};
196+
use hotfix::message::logout::Logout;
197+
use hotfix_message::fix44::MsgType;
198+
199+
let (session, mut counterparty) = crate::common::setup::given_an_active_session().await;
200+
201+
// initiate logout from our side — we stay in AwaitingLogout until the peer replies
202+
when(&session).requests_disconnect().await;
203+
then(&mut counterparty)
204+
.receives(|msg| assert_msg_type(msg, MsgType::Logout))
205+
.await;
206+
207+
let info_before = session
208+
.session_handle()
209+
.get_session_info()
210+
.await
211+
.expect("session info");
212+
213+
let result = session
214+
.session_handle()
215+
.set_next_target_seq_num(NonZeroU64::new(42).expect("42 is non-zero"))
216+
.await;
217+
218+
assert!(
219+
matches!(
220+
result,
221+
Err(SetNextTargetSeqNumError::InvalidState {
222+
current: Status::AwaitingLogout
223+
})
224+
),
225+
"expected InvalidState{{ current: AwaitingLogout }}, got: {result:?}"
226+
);
227+
228+
let info_after = session
229+
.session_handle()
230+
.get_session_info()
231+
.await
232+
.expect("session info");
233+
assert_eq!(
234+
info_after.next_target_seq_number,
235+
info_before.next_target_seq_number
236+
);
237+
238+
// let the peer reply so the session cleans up (do NOT call finally().disconnect()
239+
// — we're already in logout).
240+
when(&mut counterparty)
241+
.sends_message(Logout::default())
242+
.await;
243+
then(&mut counterparty).gets_disconnected().await;
244+
}
245+
246+
/// Rejection: while `AwaitingResend` (we detected a gap and asked the peer to
247+
/// resend), SetNextTargetSeqNum is refused and the store is untouched.
248+
#[tokio::test]
249+
async fn test_set_next_target_seq_num_rejected_while_awaiting_resend() {
250+
use crate::common::actions::when;
251+
use crate::common::test_messages::TestMessage;
252+
253+
let (mut session, mut counterparty) = crate::common::setup::given_an_active_session().await;
254+
255+
// create a gap so the session transitions to AwaitingResend
256+
when(&mut counterparty)
257+
.has_previously_sent(TestMessage::dummy_execution_report())
258+
.await;
259+
when(&mut counterparty)
260+
.sends_message(TestMessage::dummy_execution_report())
261+
.await;
262+
263+
crate::common::assertions::then(&mut session)
264+
.status_changes_to(Status::AwaitingResend {
265+
begin: 2,
266+
end: 3,
267+
attempts: 1,
268+
})
269+
.await;
270+
crate::common::assertions::then(&mut counterparty)
271+
.receives(|msg| {
272+
crate::common::assertions::assert_msg_type(
273+
msg,
274+
hotfix_message::fix44::MsgType::ResendRequest,
275+
)
276+
})
277+
.await;
278+
279+
let info_before = session
280+
.session_handle()
281+
.get_session_info()
282+
.await
283+
.expect("session info");
284+
285+
let result = session
286+
.session_handle()
287+
.set_next_target_seq_num(NonZeroU64::new(42).expect("42 is non-zero"))
288+
.await;
289+
290+
assert!(
291+
matches!(
292+
result,
293+
Err(SetNextTargetSeqNumError::InvalidState {
294+
current: Status::AwaitingResend { .. }
295+
})
296+
),
297+
"expected InvalidState{{ current: AwaitingResend{{..}} }}, got: {result:?}"
298+
);
299+
300+
let info_after = session
301+
.session_handle()
302+
.get_session_info()
303+
.await
304+
.expect("session info");
305+
assert_eq!(
306+
info_after.next_target_seq_number,
307+
info_before.next_target_seq_number
308+
);
309+
310+
crate::common::cleanup::finally(&session, &mut counterparty)
311+
.disconnect()
312+
.await;
313+
}

0 commit comments

Comments
 (0)