@@ -2,7 +2,7 @@ use crate::common::actions::when;
22use crate :: common:: assertions:: { assert_msg_type, then} ;
33use crate :: common:: cleanup:: finally;
44use crate :: common:: setup:: given_an_active_session;
5- use hotfix:: session:: Status ;
5+ use hotfix:: session:: { SetNextTargetSeqNumError , Status } ;
66use hotfix_message:: Part ;
77use hotfix_message:: fix44:: { MsgType , RESET_SEQ_NUM_FLAG } ;
88use 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