@@ -163,3 +163,229 @@ pub(crate) async fn handle_original_sending_time_missing<A, S: MessageStore>(
163163 error ! ( "failed to increment target seq number: {:?}" , err) ;
164164 }
165165}
166+
167+ #[ cfg( test) ]
168+ mod tests {
169+ use super :: * ;
170+ use crate :: session:: test_utils:: {
171+ FakeMessageStore , create_test_ctx, create_writer, extract_field, extract_msg_type,
172+ } ;
173+ use crate :: transport:: writer:: WriterMessage ;
174+
175+ #[ tokio:: test]
176+ async fn handle_incorrect_begin_string_returns_transition_to_disconnected ( ) {
177+ let mut ctx = create_test_ctx ( FakeMessageStore :: new ( ) ) ;
178+ let ( writer, mut rx) = create_writer ( ) ;
179+
180+ let result = handle_incorrect_begin_string ( & mut ctx, & writer, "FIX.4.0" . to_string ( ) ) . await ;
181+
182+ assert ! ( matches!(
183+ result,
184+ TransitionResult :: TransitionTo ( SessionState :: Disconnected ( _) )
185+ ) ) ;
186+
187+ // Should send a Logout containing the bad begin string, then disconnect
188+ let msg = rx. recv ( ) . await . unwrap ( ) ;
189+ match & msg {
190+ WriterMessage :: SendMessage ( raw) => {
191+ assert_eq ! ( extract_msg_type( raw. as_bytes( ) ) . as_deref( ) , Some ( "5" ) ) ;
192+ let text = extract_field ( raw. as_bytes ( ) , 58 ) . expect ( "expected Text(58) field" ) ;
193+ assert ! (
194+ text. contains( "FIX.4.0" ) ,
195+ "logout text should mention the bad begin string, got: {text}"
196+ ) ;
197+ }
198+ _ => panic ! ( "expected SendMessage, got {msg:?}" ) ,
199+ }
200+ assert ! ( matches!(
201+ rx. recv( ) . await . unwrap( ) ,
202+ WriterMessage :: Disconnect
203+ ) ) ;
204+
205+ // Sender seq number should have been incremented for the logout
206+ assert_eq ! ( ctx. store. next_sender_seq, 2 ) ;
207+ }
208+
209+ #[ tokio:: test]
210+ async fn handle_incorrect_comp_id_returns_transition_to_disconnected ( ) {
211+ let mut ctx = create_test_ctx ( FakeMessageStore :: new ( ) ) ;
212+ let ( writer, mut rx) = create_writer ( ) ;
213+
214+ let result = handle_incorrect_comp_id (
215+ & mut ctx,
216+ & writer,
217+ "BAD_COMP" . to_string ( ) ,
218+ CompIdType :: Sender ,
219+ 1 ,
220+ )
221+ . await ;
222+
223+ assert ! ( matches!(
224+ result,
225+ TransitionResult :: TransitionTo ( SessionState :: Disconnected ( _) )
226+ ) ) ;
227+
228+ // First message: Reject (35=3) mentioning the bad comp ID
229+ let msg = rx. recv ( ) . await . unwrap ( ) ;
230+ match & msg {
231+ WriterMessage :: SendMessage ( raw) => {
232+ assert_eq ! ( extract_msg_type( raw. as_bytes( ) ) . as_deref( ) , Some ( "3" ) ) ;
233+ let text = extract_field ( raw. as_bytes ( ) , 58 ) . expect ( "expected Text(58) field" ) ;
234+ assert ! (
235+ text. contains( "BAD_COMP" ) ,
236+ "reject text should mention the bad comp ID, got: {text}"
237+ ) ;
238+ }
239+ _ => panic ! ( "expected SendMessage(Reject), got {msg:?}" ) ,
240+ }
241+
242+ // Second message: Logout (35=5)
243+ let msg = rx. recv ( ) . await . unwrap ( ) ;
244+ match & msg {
245+ WriterMessage :: SendMessage ( raw) => {
246+ assert_eq ! ( extract_msg_type( raw. as_bytes( ) ) . as_deref( ) , Some ( "5" ) ) ;
247+ }
248+ _ => panic ! ( "expected SendMessage(Logout), got {msg:?}" ) ,
249+ }
250+
251+ // Third: Disconnect
252+ assert ! ( matches!(
253+ rx. recv( ) . await . unwrap( ) ,
254+ WriterMessage :: Disconnect
255+ ) ) ;
256+
257+ // Sender seq incremented twice (reject + logout)
258+ assert_eq ! ( ctx. store. next_sender_seq, 3 ) ;
259+ }
260+
261+ #[ tokio:: test]
262+ async fn handle_sequence_number_too_low_possible_duplicate_returns_stay ( ) {
263+ let mut ctx = create_test_ctx ( FakeMessageStore :: new ( ) ) ;
264+ let ( writer, mut rx) = create_writer ( ) ;
265+
266+ let result = handle_sequence_number_too_low ( & mut ctx, & writer, 5 , 1 , true ) . await ;
267+
268+ assert ! ( matches!( result, TransitionResult :: Stay ) ) ;
269+
270+ // No messages should have been sent
271+ assert ! ( rx. try_recv( ) . is_err( ) ) ;
272+
273+ // Store should be untouched
274+ assert_eq ! ( ctx. store. next_sender_seq, 1 ) ;
275+ assert_eq ! ( ctx. store. next_target_seq, 1 ) ;
276+ }
277+
278+ #[ tokio:: test]
279+ async fn handle_sequence_number_too_low_returns_transition_to_disconnected_without_reconnect ( ) {
280+ let mut ctx = create_test_ctx ( FakeMessageStore :: new ( ) ) ;
281+ let ( writer, mut rx) = create_writer ( ) ;
282+
283+ let result = handle_sequence_number_too_low ( & mut ctx, & writer, 5 , 1 , false ) . await ;
284+
285+ match result {
286+ TransitionResult :: TransitionTo ( state) => {
287+ assert ! ( !state. should_reconnect( ) ) ;
288+ }
289+ TransitionResult :: Stay => panic ! ( "expected TransitionTo(Disconnected)" ) ,
290+ }
291+
292+ // Should send a Logout mentioning the sequence mismatch, then disconnect
293+ let msg = rx. recv ( ) . await . unwrap ( ) ;
294+ match & msg {
295+ WriterMessage :: SendMessage ( raw) => {
296+ assert_eq ! ( extract_msg_type( raw. as_bytes( ) ) . as_deref( ) , Some ( "5" ) ) ;
297+ let text = extract_field ( raw. as_bytes ( ) , 58 ) . expect ( "expected Text(58) field" ) ;
298+ assert ! (
299+ text. contains( "5" ) && text. contains( "1" ) ,
300+ "logout text should mention expected/actual seq nums, got: {text}"
301+ ) ;
302+ }
303+ _ => panic ! ( "expected SendMessage(Logout), got {msg:?}" ) ,
304+ }
305+ assert ! ( matches!(
306+ rx. recv( ) . await . unwrap( ) ,
307+ WriterMessage :: Disconnect
308+ ) ) ;
309+
310+ assert_eq ! ( ctx. store. next_sender_seq, 2 ) ;
311+ }
312+
313+ #[ tokio:: test]
314+ async fn handle_sending_time_accuracy_problem_sends_reject ( ) {
315+ let mut ctx = create_test_ctx ( FakeMessageStore :: new ( ) ) ;
316+ let ( writer, mut rx) = create_writer ( ) ;
317+
318+ handle_sending_time_accuracy_problem ( & mut ctx, & writer, 42 , "bad time" ) . await ;
319+
320+ let msg = rx. recv ( ) . await . unwrap ( ) ;
321+ match & msg {
322+ WriterMessage :: SendMessage ( raw) => {
323+ assert_eq ! ( extract_msg_type( raw. as_bytes( ) ) . as_deref( ) , Some ( "3" ) ) ;
324+ let text = extract_field ( raw. as_bytes ( ) , 58 ) . expect ( "expected Text(58) field" ) ;
325+ assert ! (
326+ text. contains( "bad time" ) ,
327+ "reject text should contain the provided text, got: {text}"
328+ ) ;
329+ }
330+ _ => panic ! ( "expected SendMessage(Reject), got {msg:?}" ) ,
331+ }
332+
333+ // Target seq number should have been incremented
334+ assert_eq ! ( ctx. store. next_target_seq, 2 ) ;
335+ // Sender seq number should have been incremented for the outbound reject
336+ assert_eq ! ( ctx. store. next_sender_seq, 2 ) ;
337+ }
338+
339+ #[ tokio:: test]
340+ async fn handle_original_sending_time_missing_sends_reject ( ) {
341+ let mut ctx = create_test_ctx ( FakeMessageStore :: new ( ) ) ;
342+ let ( writer, mut rx) = create_writer ( ) ;
343+
344+ handle_original_sending_time_missing ( & mut ctx, & writer, 7 ) . await ;
345+
346+ let msg = rx. recv ( ) . await . unwrap ( ) ;
347+ match & msg {
348+ WriterMessage :: SendMessage ( raw) => {
349+ assert_eq ! ( extract_msg_type( raw. as_bytes( ) ) . as_deref( ) , Some ( "3" ) ) ;
350+ let text = extract_field ( raw. as_bytes ( ) , 58 ) . expect ( "expected Text(58) field" ) ;
351+ assert ! (
352+ text. contains( "original sending time" ) ,
353+ "reject text should mention original sending time, got: {text}"
354+ ) ;
355+ }
356+ _ => panic ! ( "expected SendMessage(Reject), got {msg:?}" ) ,
357+ }
358+
359+ // Both sender and target seq numbers should have been incremented
360+ assert_eq ! ( ctx. store. next_sender_seq, 2 ) ;
361+ assert_eq ! ( ctx. store. next_target_seq, 2 ) ;
362+ }
363+
364+ #[ tokio:: test]
365+ async fn handle_invalid_msg_type_sends_reject_for_message_with_seq_num ( ) {
366+ let mut ctx = create_test_ctx ( FakeMessageStore :: new ( ) ) ;
367+ let ( writer, mut rx) = create_writer ( ) ;
368+
369+ let mut message = Message :: new ( "FIX.4.4" , "ZZ" ) ;
370+ message. header_mut ( ) . set ( MSG_SEQ_NUM , 1u64 ) ;
371+
372+ handle_invalid_msg_type ( & mut ctx, & writer, & message, "ZZ" ) . await ;
373+
374+ let msg = rx. recv ( ) . await . unwrap ( ) ;
375+ match & msg {
376+ WriterMessage :: SendMessage ( raw) => {
377+ assert_eq ! ( extract_msg_type( raw. as_bytes( ) ) . as_deref( ) , Some ( "3" ) ) ;
378+ let text = extract_field ( raw. as_bytes ( ) , 58 ) . expect ( "expected Text(58) field" ) ;
379+ assert ! (
380+ text. contains( "ZZ" ) ,
381+ "reject text should mention the invalid msg type, got: {text}"
382+ ) ;
383+ }
384+ _ => panic ! ( "expected SendMessage(Reject), got {msg:?}" ) ,
385+ }
386+
387+ // Sender seq incremented for the reject, target seq incremented because msg seq matched
388+ assert_eq ! ( ctx. store. next_sender_seq, 2 ) ;
389+ assert_eq ! ( ctx. store. next_target_seq, 2 ) ;
390+ }
391+ }
0 commit comments