Skip to content

Commit 8dbc641

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

2 files changed

Lines changed: 221 additions & 12 deletions

File tree

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

0 commit comments

Comments
 (0)